diff options
Diffstat (limited to 'core/tests/Drupal/KernelTests')
38 files changed, 3397 insertions, 1104 deletions
diff --git a/core/tests/Drupal/KernelTests/Components/ComponentPluginManagerCachedDiscoveryTest.php b/core/tests/Drupal/KernelTests/Components/ComponentPluginManagerCachedDiscoveryTest.php new file mode 100644 index 000000000000..1b012c9f7262 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Components/ComponentPluginManagerCachedDiscoveryTest.php @@ -0,0 +1,38 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Components; + +/** + * Tests discovery of components in a theme being installed or uninstalled. + * + * @group sdc + */ +class ComponentPluginManagerCachedDiscoveryTest extends ComponentKernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $themes = ['stark']; + + /** + * Tests cached component plugin discovery on theme install and uninstall. + */ + public function testComponentDiscoveryOnThemeInstall(): void { + // Component in sdc_theme should not be found without sdc_theme installed. + $definitions = \Drupal::service('plugin.manager.sdc')->getDefinitions(); + $this->assertArrayNotHasKey('sdc_theme_test:bar', $definitions); + + // Component in sdc_theme should be found once sdc_theme is installed. + \Drupal::service('theme_installer')->install(['sdc_theme_test']); + $definitions = \Drupal::service('plugin.manager.sdc')->getDefinitions(); + $this->assertArrayHasKey('sdc_theme_test:bar', $definitions); + + // Component in sdc_theme should not be found once sdc_theme is uninstalled. + \Drupal::service('theme_installer')->uninstall(['sdc_theme_test']); + $definitions = \Drupal::service('plugin.manager.sdc')->getDefinitions(); + $this->assertArrayNotHasKey('sdc_theme_test:bar', $definitions); + } + +} diff --git a/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php b/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php index 3ca62dcced59..f375f6f3dd69 100644 --- a/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php +++ b/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php @@ -377,6 +377,7 @@ class MappingTest extends KernelTestBase { 'field.value.decimal' => ['value'], 'field.value.float' => ['value'], 'field.value.timestamp' => ['value'], + 'field.value.language' => ['value'], 'field.value.comment' => [ 'status', 'cid', diff --git a/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php b/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php index 6dd535e88f95..809e54737b7c 100644 --- a/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php +++ b/core/tests/Drupal/KernelTests/Core/Asset/AttachedAssetsTest.php @@ -467,4 +467,26 @@ class AttachedAssetsTest extends KernelTestBase { $this->assertStringContainsString('<script src="' . str_replace('&', '&', $this->fileUrlGenerator->generateString('core/modules/system/tests/modules/common_test/querystring.js?arg1=value1&arg2=value2')) . '&' . $query_string . '"></script>', $rendered_js, 'JavaScript file with query string gets version query string correctly appended.'); } + /** + * Test settings can be loaded even when libraries are not. + */ + public function testAttachedSettingsWithoutLibraries(): void { + $assets = new AttachedAssets(); + + // First test with no libraries will return no settings. + $assets->setSettings(['test' => 'foo']); + $js = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage())[1]; + $this->assertArrayNotHasKey('drupalSettings', $js); + + // Second test with a warm cache. + $js = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage())[1]; + $this->assertArrayNotHasKey('drupalSettings', $js); + + // Now test with different settings when drupalSettings is already loaded. + $assets->setSettings(['test' => 'bar']); + $assets->setAlreadyLoadedLibraries(['core/drupalSettings']); + $js = $this->assetResolver->getJsAssets($assets, TRUE, \Drupal::languageManager()->getCurrentLanguage())[1]; + $this->assertSame('bar', $js['drupalSettings']['data']['test']); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php b/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php index 340dcc286494..15c97bea71f4 100644 --- a/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php +++ b/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php @@ -99,6 +99,20 @@ class ResolvedLibraryDefinitionsFilesMatchTest extends KernelTestBase { protected function setUp(): void { parent::setUp(); + // Install all core themes. + sort($this->allThemes); + $this->container->get('theme_installer')->install($this->allThemes); + + $this->themeHandler = $this->container->get('theme_handler'); + $this->themeInitialization = $this->container->get('theme.initialization'); + $this->themeManager = $this->container->get('theme.manager'); + $this->libraryDiscovery = $this->container->get('library.discovery'); + } + + /** + * Ensures that all core module and theme library files exist. + */ + public function testCoreLibraryCompleteness(): void { // Enable all core modules. $all_modules = $this->container->get('extension.list.module')->getList(); $all_modules = array_filter($all_modules, function ($module) { @@ -141,21 +155,37 @@ class ResolvedLibraryDefinitionsFilesMatchTest extends KernelTestBase { } sort($this->allModules); $this->container->get('module_installer')->install($this->allModules); + // Get a library discovery from the new container. + $this->libraryDiscovery = $this->container->get('library.discovery'); - // Install all core themes. - sort($this->allThemes); - $this->container->get('theme_installer')->install($this->allThemes); + $this->assertLibraries(); + } - $this->themeHandler = $this->container->get('theme_handler'); - $this->themeInitialization = $this->container->get('theme.initialization'); - $this->themeManager = $this->container->get('theme.manager'); + /** + * Ensures that module and theme library files exist for a deprecated modules. + * + * @group legacy + */ + public function testCoreLibraryCompletenessDeprecated(): void { + // Find and install deprecated modules to test. + $all_modules = $this->container->get('extension.list.module')->getList(); + $deprecated_modules_to_test = array_filter($all_modules, function ($module) { + if ($module->origin == 'core' + && $module->info[ExtensionLifecycle::LIFECYCLE_IDENTIFIER] === ExtensionLifecycle::DEPRECATED) { + return TRUE; + } + }); + $this->container->get('module_installer')->install(array_keys($deprecated_modules_to_test)); $this->libraryDiscovery = $this->container->get('library.discovery'); + $this->allModules = array_keys(\Drupal::moduleHandler()->getModuleList()); + + $this->assertLibraries(); } /** - * Ensures that all core module and theme library files exist. + * Asserts the libraries for modules and themes exist. */ - public function testCoreLibraryCompleteness(): void { + public function assertLibraries(): void { // First verify all libraries with no active theme. $this->verifyLibraryFilesExist($this->getAllLibraries()); diff --git a/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php b/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php index c2bff1232364..c1ff7e24406a 100644 --- a/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php +++ b/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\KernelTests\Core\Cache; use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheTagsPurgeInterface; use Drupal\Core\Database\Database; use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\KernelTests\KernelTestBase; @@ -62,4 +63,34 @@ class DatabaseBackendTagTest extends KernelTestBase { $this->assertEquals($invalidations_before + 1, $invalidations_after, 'Only one addition cache tag invalidation has occurred after invalidating a tag used in multiple bins.'); } + /** + * Test cache tag purging. + */ + public function testTagsPurge(): void { + $tags = ['test_tag:1', 'test_tag:2', 'test_tag:3']; + /** @var \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_invalidator */ + $checksum_invalidator = \Drupal::service('cache_tags.invalidator.checksum'); + // Assert that initial current tag checksum is 0. This also ensures that the + // 'cachetags' table is created, which at this point does not exist yet. + $this->assertEquals(0, $checksum_invalidator->getCurrentChecksum($tags)); + + /** @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface $invalidator */ + $invalidator = \Drupal::service('cache_tags.invalidator'); + $invalidator->invalidateTags($tags); + // Checksum should be incremented by 1 by the invalidation for each tag. + $this->assertEquals(3, $checksum_invalidator->getCurrentChecksum($tags)); + + // After purging, confirm checksum is 0 and the 'cachetags' table is empty. + $this->assertInstanceOf(CacheTagsPurgeInterface::class, $invalidator); + $invalidator->purge(); + $this->assertEquals(0, $checksum_invalidator->getCurrentChecksum($tags)); + + $rows = Database::getConnection()->select('cachetags') + ->fields('cachetags') + ->countQuery() + ->execute() + ->fetchField(); + $this->assertEmpty($rows, 'cachetags table is empty.'); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php index bcdc76cc71d7..3d3ee862cef6 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigExistsConstraintValidatorTest.php @@ -28,6 +28,7 @@ class ConfigExistsConstraintValidatorTest extends KernelTestBase { * * @testWith [{}, "system.site", "system.site"] * [{"prefix": "system."}, "site", "system.site"] + * [{"prefix": "system.[%parent.reference]."}, "admin", "system.menu.admin"] */ public function testValidation(array $constraint_options, string $value, string $expected_config_name): void { // Create a data definition that specifies the value must be a string with @@ -37,7 +38,11 @@ class ConfigExistsConstraintValidatorTest extends KernelTestBase { /** @var \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data */ $typed_data = $this->container->get('typed_data_manager'); - $data = $typed_data->create($definition, $value); + + // Create a data definition for the parent data. + $parent_data_definition = $typed_data->createDataDefinition('map'); + $parent_data = $typed_data->create($parent_data_definition, ['reference' => 'menu']); + $data = $typed_data->create($definition, $value, 'data_name', $parent_data); $violations = $data->validate(); $this->assertCount(1, $violations); diff --git a/core/tests/Drupal/KernelTests/Core/Controller/ControllerBaseTest.php b/core/tests/Drupal/KernelTests/Core/Controller/ControllerBaseTest.php index 91cc24234e1a..9882d3d9ef0f 100644 --- a/core/tests/Drupal/KernelTests/Core/Controller/ControllerBaseTest.php +++ b/core/tests/Drupal/KernelTests/Core/Controller/ControllerBaseTest.php @@ -4,8 +4,10 @@ declare(strict_types=1); namespace Drupal\KernelTests\Core\Controller; +use Drupal\dblog\Logger\DbLog; use Drupal\KernelTests\KernelTestBase; use Drupal\system_test\Controller\BrokenSystemTestController; +use Drupal\system_test\Controller\OptionalServiceSystemTestController; use Drupal\system_test\Controller\SystemTestController; use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException; @@ -52,4 +54,17 @@ class ControllerBaseTest extends KernelTestBase { $this->container->get('class_resolver')->getInstanceFromDefinition(BrokenSystemTestController::class); } + /** + * @covers ::create + */ + public function testCreateOptional(): void { + $service = $this->container->get('class_resolver')->getInstanceFromDefinition(OptionalServiceSystemTestController::class); + $this->assertInstanceOf(OptionalServiceSystemTestController::class, $service); + $this->assertNull($service->dbLog); + $this->container->get('module_installer')->install(['dblog']); + $service = $this->container->get('class_resolver')->getInstanceFromDefinition(OptionalServiceSystemTestController::class); + $this->assertInstanceOf(OptionalServiceSystemTestController::class, $service); + $this->assertInstanceOf(DbLog::class, $service->dbLog); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php b/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php index 079009dc7a56..8725f647e8d1 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/BasicSyntaxTest.php @@ -63,18 +63,6 @@ class BasicSyntaxTest extends DatabaseTestBase { } /** - * Tests string concatenation with separator, with field values. - */ - public function testConcatWsFields(): void { - $result = $this->connection->query("SELECT CONCAT_WS('-', :a1, [name], :a2, [age]) FROM {test} WHERE [age] = :age", [ - ':a1' => 'name', - ':a2' => 'age', - ':age' => 25, - ]); - $this->assertSame('name-John-age-25', $result->fetchField()); - } - - /** * Tests escaping of LIKE wildcards. */ public function testLikeEscape(): void { diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php index ca5fb32936b5..a3b46ab67e12 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php +++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSchemaTestBase.php @@ -272,6 +272,16 @@ abstract class DriverSpecificSchemaTestBase extends DriverSpecificKernelTestBase // Test the primary key columns. $this->assertSame(['test_serial', 'test_composite_primary_key'], $method->invoke($this->schema, 'test_table')); + // Test adding and removing JSON column. + $this->schema->addField('test_table', 'test_json', [ + 'description' => 'I heard you liked JSON.', + 'type' => 'json', + 'pgsql_type' => 'jsonb', + 'mysql_type' => 'json', + 'sqlite_type' => 'json', + ]); + $this->schema->dropField('test_table', 'test_json'); + // Test renaming of keys and constraints. $this->schema->dropTable('test_table'); $table_specification = [ diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php index 7723d872cc12..e18336205398 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php +++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificSyntaxTestBase.php @@ -43,4 +43,16 @@ abstract class DriverSpecificSyntaxTestBase extends DriverSpecificDatabaseTestBa $this->assertSame('[square]', $result->fetchField()); } + /** + * Tests string concatenation with separator, with field values. + */ + public function testConcatWsFields(): void { + $result = $this->connection->query("SELECT CONCAT_WS('-', :a1, [name], :a2, [age]) FROM {test} WHERE [age] = :age", [ + ':a1' => 'name', + ':a2' => 'age', + ':age' => 25, + ]); + $this->assertSame('name-John-age-25', $result->fetchField()); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php index c361c7af9596..b7e31e06a59a 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php +++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php @@ -38,7 +38,7 @@ use Drupal\Core\Database\TransactionOutOfOrderException; * is active, and mysqli does not fail when rolling back and no transaction * active. */ -class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase { +abstract class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase { /** * Keeps track of the post-transaction callback action executed. @@ -432,9 +432,6 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase { /** * Tests rollback after a DDL statement when no transactional DDL supported. - * - * @todo In drupal:12.0.0, rollBack will throw a - * TransactionOutOfOrderException. Adjust the test accordingly. */ public function testRollbackAfterDdlStatementForNonTransactionalDdlDatabase(): void { if ($this->connection->supportsTransactionalDDL()) { @@ -919,9 +916,6 @@ class DriverSpecificTransactionTestBase extends DriverSpecificDatabaseTestBase { * transaction including DDL statements is not possible, since a commit * happened already. We cannot decide what should be the status of the * callback, an exception is thrown. - * - * @todo In drupal:12.0.0, rollBack will throw a - * TransactionOutOfOrderException. Adjust the test accordingly. */ public function testRootTransactionEndCallbackFailureUponDdlAndRollbackForNonTransactionalDdlDatabase(): void { if ($this->connection->supportsTransactionalDDL()) { diff --git a/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php b/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php index 6e28787f764c..3bfaa18889a9 100644 --- a/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php +++ b/core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php @@ -20,9 +20,9 @@ class FetchLegacyTest extends DatabaseTestBase { */ #[IgnoreDeprecations] public function testQueryFetchObject(): void { - $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338"); - $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338"); - $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338"); + $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338"); + $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338"); + $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338"); $records = []; $result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_OBJ]); foreach ($result as $record) { @@ -39,9 +39,9 @@ class FetchLegacyTest extends DatabaseTestBase { */ #[IgnoreDeprecations] public function testQueryFetchArray(): void { - $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338"); - $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338"); - $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338"); + $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338"); + $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338"); + $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338"); $records = []; $result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_ASSOC]); foreach ($result as $record) { @@ -59,9 +59,9 @@ class FetchLegacyTest extends DatabaseTestBase { */ #[IgnoreDeprecations] public function testQueryFetchNum(): void { - $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338"); - $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338"); - $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338"); + $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in query() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338"); + $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in prepareStatement() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338"); + $this->expectDeprecation("Passing the 'fetch' key as an integer to \$options in execute() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338"); $records = []; $result = $this->connection->query('SELECT [name] FROM {test} WHERE [age] = :age', [':age' => 25], ['fetch' => \PDO::FETCH_NUM]); foreach ($result as $record) { @@ -79,7 +79,7 @@ class FetchLegacyTest extends DatabaseTestBase { */ #[IgnoreDeprecations] public function testQueryFetchAllColumn(): void { - $this->expectDeprecation("Passing the \$mode argument as an integer to fetchAll() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338"); + $this->expectDeprecation("Passing the \$mode argument as an integer to fetchAll() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338"); $query = $this->connection->select('test'); $query->addField('test', 'name'); $query->orderBy('name'); @@ -94,7 +94,7 @@ class FetchLegacyTest extends DatabaseTestBase { */ #[IgnoreDeprecations] public function testQueryFetchAllAssoc(): void { - $this->expectDeprecation("Passing the \$fetch argument as an integer to fetchAllAssoc() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\FetchAs enum instead. See https://www.drupal.org/node/3488338"); + $this->expectDeprecation("Passing the \$fetch argument as an integer to fetchAllAssoc() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use a case of \Drupal\Core\Database\Statement\FetchAs enum instead. See https://www.drupal.org/node/3488338"); $expected_result = [ "Singer" => [ "id" => "2", diff --git a/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php b/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php new file mode 100644 index 000000000000..f40a977f6f49 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php @@ -0,0 +1,1278 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Database; + +use Drupal\Core\Database\Database; +use Drupal\Core\Database\Transaction; +use Drupal\Core\Database\Transaction\ClientConnectionTransactionState; +use Drupal\Core\Database\Transaction\StackItem; +use Drupal\Core\Database\Transaction\StackItemType; +use Drupal\Core\Database\Transaction\TransactionManagerBase; +use Drupal\Core\Database\TransactionNameNonUniqueException; +use Drupal\Core\Database\TransactionOutOfOrderException; + +// cspell:ignore Tinky Winky Dipsy + +/** + * Tests the transactions, using the explicit ::commitOrRelease method. + * + * We test nesting by having two transaction layers, an outer and inner. The + * outer layer encapsulates the inner layer. Our transaction nesting abstraction + * should allow the outer layer function to call any function it wants, + * especially the inner layer that starts its own transaction, and be + * confident that, when the function it calls returns, its own transaction + * is still "alive." + * + * Call structure: + * transactionOuterLayer() + * Start transaction "A" + * transactionInnerLayer() + * Start transaction "B" (does nothing in database) + * [Maybe decide to roll back "B"] + * Do more stuff + * Should still be in transaction "A" + * + * These method can be overridden by non-core database driver if their + * transaction behavior is different from core. For example, both oci8 (Oracle) + * and mysqli (MySql) clients do not have a solution to check if a transaction + * is active, and mysqli does not fail when rolling back and no transaction + * active. + * + * @group Database + */ +class TransactionTest extends DatabaseTestBase { + + /** + * Keeps track of the post-transaction callback action executed. + */ + protected ?string $postTransactionCallbackAction = NULL; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + // Set the transaction manager to trigger warnings when appropriate. + $this->connection->transactionManager()->triggerWarningWhenUnpilingOnVoidTransaction = TRUE; + } + + /** + * Create a root Drupal transaction. + */ + protected function createRootTransaction(string $name = '', bool $insertRow = TRUE): Transaction { + $this->assertFalse($this->connection->inTransaction()); + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + + // Start root transaction. Corresponds to 'BEGIN TRANSACTION' on the + // database. + $transaction = $this->connection->startTransaction($name); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Insert a single row into the testing table. + if ($insertRow) { + $this->insertRow('David'); + $this->assertRowPresent('David'); + } + + return $transaction; + } + + /** + * Create a Drupal savepoint transaction after root. + */ + protected function createFirstSavepointTransaction(string $name = '', bool $insertRow = TRUE): Transaction { + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_1' + // on the database. The name can be changed by the $name argument. + $savepoint = $this->connection->startTransaction($name); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(2, $this->connection->transactionManager()->stackDepth()); + + // Insert a single row into the testing table. + if ($insertRow) { + $this->insertRow('Roger'); + $this->assertRowPresent('Roger'); + } + + return $savepoint; + } + + /** + * Encapsulates a transaction's "inner layer" with an "outer layer". + * + * This "outer layer" transaction starts and then encapsulates the "inner + * layer" transaction. This nesting is used to evaluate whether the database + * transaction API properly supports nesting. By "properly supports," we mean + * the outer transaction continues to exist regardless of what functions are + * called and whether those functions start their own transactions. + * + * In contrast, a typical database would commit the outer transaction, start + * a new transaction for the inner layer, commit the inner layer transaction, + * and then be confused when the outer layer transaction tries to commit its + * transaction (which was already committed when the inner transaction + * started). + * + * @param string $suffix + * Suffix to add to field values to differentiate tests. + */ + protected function transactionOuterLayer(string $suffix): void { + $txn = $this->connection->startTransaction(); + + // Insert a single row into the testing table. + $this->connection->insert('test') + ->fields([ + 'name' => 'David' . $suffix, + 'age' => '24', + ]) + ->execute(); + + $this->assertTrue($this->connection->inTransaction(), 'In transaction before calling nested transaction.'); + + // We're already in a transaction, but we call ->transactionInnerLayer + // to nest another transaction inside the current one. + $this->transactionInnerLayer($suffix); + + $this->assertTrue($this->connection->inTransaction(), 'In transaction after calling nested transaction.'); + + $txn->commitOrRelease(); + } + + /** + * Creates an "inner layer" transaction. + * + * This "inner layer" transaction is either used alone or nested inside of the + * "outer layer" transaction. + * + * @param string $suffix + * Suffix to add to field values to differentiate tests. + */ + protected function transactionInnerLayer(string $suffix): void { + $depth = $this->connection->transactionManager()->stackDepth(); + // Start a transaction. If we're being called from ->transactionOuterLayer, + // then we're already in a transaction. Normally, that would make starting + // a transaction here dangerous, but the database API handles this problem + // for us by tracking the nesting and avoiding the danger. + $txn = $this->connection->startTransaction(); + + $depth2 = $this->connection->transactionManager()->stackDepth(); + $this->assertSame($depth + 1, $depth2, 'Transaction depth has increased with new transaction.'); + + // Insert a single row into the testing table. + $this->connection->insert('test') + ->fields([ + 'name' => 'Daniel' . $suffix, + 'age' => '19', + ]) + ->execute(); + + $this->assertTrue($this->connection->inTransaction(), 'In transaction inside nested transaction.'); + + $txn->commitOrRelease(); + } + + /** + * Tests root transaction rollback. + */ + public function testRollbackRoot(): void { + $transaction = $this->createRootTransaction(); + + // Rollback. Since we are at the root, the transaction is closed. + // Corresponds to 'ROLLBACK' on the database. + $transaction->rollBack(); + $this->assertRowAbsent('David'); + $this->assertFalse($this->connection->inTransaction()); + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + } + + /** + * Tests root transaction rollback after savepoint rollback. + */ + public function testRollbackRootAfterSavepointRollback(): void { + $transaction = $this->createRootTransaction(); + $savepoint = $this->createFirstSavepointTransaction(); + + // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK + // TO savepoint_1' plus 'RELEASE savepoint_1' on the database. + $savepoint->rollBack(); + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Try to rollback root. No savepoint is active, this should succeed. + $transaction->rollBack(); + $this->assertRowAbsent('David'); + $this->assertRowAbsent('Roger'); + $this->assertFalse($this->connection->inTransaction()); + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + } + + /** + * Tests root transaction rollback failure when savepoint is open. + */ + public function testRollbackRootWithActiveSavepoint(): void { + $transaction = $this->createRootTransaction(); + // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis + $savepoint = $this->createFirstSavepointTransaction(); + + // Try to rollback root. Since a savepoint is active, this should fail. + $this->expectException(TransactionOutOfOrderException::class); + $this->expectExceptionMessageMatches("/^Error attempting rollback of .*\\\\drupal_transaction\\. Active stack: .*\\\\drupal_transaction > .*\\\\savepoint_1/"); + $transaction->rollBack(); + } + + /** + * Tests savepoint transaction rollback. + */ + public function testRollbackSavepoint(): void { + $transaction = $this->createRootTransaction(); + $savepoint = $this->createFirstSavepointTransaction(); + + // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK + // TO savepoint_1' plus 'RELEASE savepoint_1' on the database. + $savepoint->rollBack(); + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Insert a row. + $this->insertRow('Syd'); + + // Commit root. + $transaction->commitOrRelease(); + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertRowPresent('Syd'); + $this->assertFalse($this->connection->inTransaction()); + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + } + + /** + * Tests savepoint transaction commit after rollback. + */ + public function testCommitAfterRollbackSameSavepoint(): void { + $transaction = $this->createRootTransaction(); + $savepoint = $this->createFirstSavepointTransaction(); + + // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK + // TO savepoint_1' plus 'RELEASE savepoint_1' on the database. + $savepoint->rollBack(); + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Insert a row. + $this->insertRow('Syd'); + + // Try releasing savepoint. Should fail since it was released already. + try { + $savepoint->commitOrRelease(); + $this->fail('Expected TransactionOutOfOrderException was not thrown'); + } + catch (\Exception $e) { + $this->assertInstanceOf(TransactionOutOfOrderException::class, $e); + $this->assertMatchesRegularExpression("/^Error attempting commit of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage()); + } + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertRowPresent('Syd'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Commit root. + $transaction->commitOrRelease(); + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertRowPresent('Syd'); + $this->assertFalse($this->connection->inTransaction()); + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + } + + /** + * Tests savepoint transaction rollback after commit. + */ + public function testRollbackAfterCommitSameSavepoint(): void { + $transaction = $this->createRootTransaction(); + $savepoint = $this->createFirstSavepointTransaction(); + + // Release savepoint. Corresponds to 'RELEASE savepoint_1' on the database. + $savepoint->commitOrRelease(); + $this->assertRowPresent('David'); + $this->assertRowPresent('Roger'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Insert a row. + $this->insertRow('Syd'); + + // Try rolling back savepoint. Should fail since it was released already. + try { + $savepoint->rollback(); + $this->fail('Expected TransactionOutOfOrderException was not thrown'); + } + catch (\Exception $e) { + $this->assertInstanceOf(TransactionOutOfOrderException::class, $e); + $this->assertMatchesRegularExpression("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage()); + } + $this->assertRowPresent('David'); + $this->assertRowPresent('Roger'); + $this->assertRowPresent('Syd'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Commit root. + $transaction->commitOrRelease(); + $this->assertRowPresent('David'); + $this->assertRowPresent('Roger'); + $this->assertRowPresent('Syd'); + $this->assertFalse($this->connection->inTransaction()); + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + } + + /** + * Tests savepoint transaction duplicated rollback. + */ + public function testRollbackTwiceSameSavepoint(): void { + // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis + $transaction = $this->createRootTransaction(); + $savepoint = $this->createFirstSavepointTransaction(); + + // Rollback savepoint. It should get released too. Corresponds to 'ROLLBACK + // TO savepoint_1' plus 'RELEASE savepoint_1' on the database. + $savepoint->rollBack(); + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + + // Insert a row. + $this->insertRow('Syd'); + + // Rollback savepoint again. Should fail since it was released already. + try { + $savepoint->rollBack(); + $this->fail('Expected TransactionOutOfOrderException was not thrown'); + } + catch (\Exception $e) { + $this->assertInstanceOf(TransactionOutOfOrderException::class, $e); + $this->assertMatchesRegularExpression("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction/", $e->getMessage()); + } + $this->assertRowPresent('David'); + $this->assertRowAbsent('Roger'); + $this->assertRowPresent('Syd'); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + } + + /** + * Tests savepoint transaction rollback failure when later savepoints exist. + */ + public function testRollbackSavepointWithLaterSavepoint(): void { + // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis + $transaction = $this->createRootTransaction(); + $savepoint1 = $this->createFirstSavepointTransaction(); + + // Starts another savepoint transaction. Corresponds to 'SAVEPOINT + // savepoint_2' on the database. + // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis + $savepoint2 = $this->connection->startTransaction(); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(3, $this->connection->transactionManager()->stackDepth()); + + // Insert a row. + $this->insertRow('Syd'); + $this->assertRowPresent('David'); + $this->assertRowPresent('Roger'); + $this->assertRowPresent('Syd'); + + // Try to rollback to savepoint 1. Out of order. + $this->expectException(TransactionOutOfOrderException::class); + $this->expectExceptionMessageMatches("/^Error attempting rollback of .*\\\\savepoint_1\\. Active stack: .*\\\\drupal_transaction > .*\\\\savepoint_1 > .*\\\\savepoint_2/"); + $savepoint1->rollBack(); + } + + /** + * Tests commit does not fail when committing after DDL. + * + * In core, SQLite and PostgreSql databases support transactional DDL, MySql + * does not. + */ + public function testCommitAfterDdl(): void { + $transaction = $this->createRootTransaction(); + $savepoint = $this->createFirstSavepointTransaction(); + + $this->executeDDLStatement(); + + $this->assertRowPresent('David'); + $this->assertRowPresent('Roger'); + if ($this->connection->supportsTransactionalDDL()) { + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(2, $this->connection->transactionManager()->stackDepth()); + } + else { + $this->assertFalse($this->connection->inTransaction()); + } + + $this->assertRowPresent('David'); + $this->assertRowPresent('Roger'); + if ($this->connection->supportsTransactionalDDL()) { + $savepoint->commitOrRelease(); + $this->assertTrue($this->connection->inTransaction()); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + } + else { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $savepoint->commitOrRelease(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + $this->assertFalse($this->connection->inTransaction()); + } + + if ($this->connection->supportsTransactionalDDL()) { + $transaction->commitOrRelease(); + } + else { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction->commitOrRelease(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + } + $this->assertRowPresent('David'); + $this->assertRowPresent('Roger'); + $this->assertFalse($this->connection->inTransaction()); + } + + /** + * Tests a committed transaction. + * + * The behavior of this test should be identical for connections that support + * transactions and those that do not. + */ + public function testCommittedTransaction(): void { + // Create two nested transactions. The changes should be committed. + $this->transactionOuterLayer('A'); + + // Because we committed, both of the inserted rows should be present. + $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'DavidA'])->fetchField(); + $this->assertSame('24', $saved_age, 'Can retrieve DavidA row after commit.'); + $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'DanielA'])->fetchField(); + $this->assertSame('19', $saved_age, 'Can retrieve DanielA row after commit.'); + } + + /** + * Tests the compatibility of transactions with DDL statements. + */ + public function testTransactionWithDdlStatement(): void { + // First, test that a commit works normally, even with DDL statements. + $transaction = $this->createRootTransaction('', FALSE); + $this->insertRow('row'); + $this->executeDDLStatement(); + if ($this->connection->supportsTransactionalDDL()) { + $transaction->commitOrRelease(); + } + else { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction->commitOrRelease(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + } + $this->assertRowPresent('row'); + + // Even in different order. + $this->cleanUp(); + $transaction = $this->createRootTransaction('', FALSE); + $this->executeDDLStatement(); + $this->insertRow('row'); + if ($this->connection->supportsTransactionalDDL()) { + $transaction->commitOrRelease(); + } + else { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction->commitOrRelease(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + } + $this->assertRowPresent('row'); + + // Even with stacking. + $this->cleanUp(); + $transaction = $this->createRootTransaction('', FALSE); + $transaction2 = $this->createFirstSavepointTransaction('', FALSE); + $this->executeDDLStatement(); + if ($this->connection->supportsTransactionalDDL()) { + $transaction2->commitOrRelease(); + } + else { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction2->commitOrRelease(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + } + $transaction3 = $this->connection->startTransaction(); + $this->insertRow('row'); + $transaction3->commitOrRelease(); + + if ($this->connection->supportsTransactionalDDL()) { + $transaction->commitOrRelease(); + } + else { + try { + $transaction->commitOrRelease(); + $this->fail('TransactionOutOfOrderException was expected, but did not throw.'); + } + catch (TransactionOutOfOrderException) { + // Just continue, this is out or order since $transaction3 started a + // new root. + } + } + $this->assertRowPresent('row'); + + // A transaction after a DDL statement should still work the same. + $this->cleanUp(); + $transaction = $this->createRootTransaction('', FALSE); + $transaction2 = $this->createFirstSavepointTransaction('', FALSE); + $this->executeDDLStatement(); + if ($this->connection->supportsTransactionalDDL()) { + $transaction2->commitOrRelease(); + } + else { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction2->commitOrRelease(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + } + $transaction3 = $this->connection->startTransaction(); + $this->insertRow('row'); + $transaction3->rollBack(); + if ($this->connection->supportsTransactionalDDL()) { + $transaction->commitOrRelease(); + } + else { + try { + $transaction->commitOrRelease(); + $this->fail('TransactionOutOfOrderException was expected, but did not throw.'); + } + catch (TransactionOutOfOrderException) { + // Just continue, this is out or order since $transaction3 started a + // new root. + } + } + $this->assertRowAbsent('row'); + + // The behavior of a rollback depends on the type of database server. + if ($this->connection->supportsTransactionalDDL()) { + // For database servers that support transactional DDL, a rollback + // of a transaction including DDL statements should be possible. + $this->cleanUp(); + $transaction = $this->createRootTransaction('', FALSE); + $this->insertRow('row'); + $this->executeDDLStatement(); + $transaction->rollBack(); + $this->assertRowAbsent('row'); + + // Including with stacking. + $this->cleanUp(); + $transaction = $this->createRootTransaction('', FALSE); + $transaction2 = $this->createFirstSavepointTransaction('', FALSE); + $this->executeDDLStatement(); + $transaction2->commitOrRelease(); + $transaction3 = $this->connection->startTransaction(); + $this->insertRow('row'); + $transaction3->commitOrRelease(); + $this->assertRowPresent('row'); + $transaction->rollBack(); + $this->assertRowAbsent('row'); + } + } + + /** + * Tests rollback after a DDL statement when no transactional DDL supported. + */ + public function testRollbackAfterDdlStatementForNonTransactionalDdlDatabase(): void { + if ($this->connection->supportsTransactionalDDL()) { + $this->markTestSkipped('This test only works for database that do not support transactional DDL.'); + } + + // For database servers that do not support transactional DDL, + // the DDL statement should commit the transaction stack. + $this->cleanUp(); + $transaction = $this->createRootTransaction('', FALSE); + $reflectionMethod = new \ReflectionMethod(get_class($this->connection->transactionManager()), 'getConnectionTransactionState'); + $this->assertSame(1, $this->connection->transactionManager()->stackDepth()); + $this->assertEquals(ClientConnectionTransactionState::Active, $reflectionMethod->invoke($this->connection->transactionManager())); + $this->insertRow('row'); + $this->executeDDLStatement(); + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + $this->assertEquals(ClientConnectionTransactionState::Voided, $reflectionMethod->invoke($this->connection->transactionManager())); + + // Try to rollback the root transaction. Since the DDL already committed + // it, it should fail. + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction->rollBack(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::rollBack() failed because of a prior execution of a DDL statement.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + + try { + $transaction->commitOrRelease(); + $this->fail('TransactionOutOfOrderException was expected, but did not throw.'); + } + catch (TransactionOutOfOrderException) { + // Just continue, the attempted rollback made the overall state to + // ClientConnectionTransactionState::RollbackFailed. + } + + $manager = $this->connection->transactionManager(); + $this->assertSame(0, $manager->stackDepth()); + $reflectedTransactionState = new \ReflectionMethod($manager, 'getConnectionTransactionState'); + $this->assertSame(ClientConnectionTransactionState::RollbackFailed, $reflectedTransactionState->invoke($manager)); + $this->assertRowPresent('row'); + } + + /** + * Inserts a single row into the testing table. + */ + protected function insertRow(string $name): void { + $this->connection->insert('test') + ->fields([ + 'name' => $name, + ]) + ->execute(); + } + + /** + * Executes a DDL statement. + */ + protected function executeDDLStatement(): void { + static $count = 0; + $table = [ + 'fields' => [ + 'id' => [ + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ], + ], + 'primary key' => ['id'], + ]; + $this->connection->schema()->createTable('database_test_' . ++$count, $table); + } + + /** + * Starts over for a new test. + */ + protected function cleanUp(): void { + $this->connection->truncate('test') + ->execute(); + $this->postTransactionCallbackAction = NULL; + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + } + + /** + * Asserts that a given row is present in the test table. + * + * @param string $name + * The name of the row. + * @param string $message + * The message to log for the assertion. + * + * @internal + */ + public function assertRowPresent(string $name, ?string $message = NULL): void { + $present = (boolean) $this->connection->query('SELECT 1 FROM {test} WHERE [name] = :name', [':name' => $name])->fetchField(); + $this->assertTrue($present, $message ?? "Row '{$name}' should be present, but it actually does not exist."); + } + + /** + * Asserts that a given row is absent from the test table. + * + * @param string $name + * The name of the row. + * @param string $message + * The message to log for the assertion. + * + * @internal + */ + public function assertRowAbsent(string $name, ?string $message = NULL): void { + $present = (boolean) $this->connection->query('SELECT 1 FROM {test} WHERE [name] = :name', [':name' => $name])->fetchField(); + $this->assertFalse($present, $message ?? "Row '{$name}' should be absent, but it actually exists."); + } + + /** + * Tests transaction stacking, commit, and rollback. + */ + public function testTransactionStacking(): void { + // Standard case: pop the inner transaction before the outer transaction. + $transaction = $this->createRootTransaction('', FALSE); + $this->insertRow('outer'); + $transaction2 = $this->createFirstSavepointTransaction('', FALSE); + $this->insertRow('inner'); + // Pop the inner transaction. + $transaction2->commitOrRelease(); + $this->assertTrue($this->connection->inTransaction(), 'Still in a transaction after popping the inner transaction'); + // Pop the outer transaction. + $transaction->commitOrRelease(); + $this->assertFalse($this->connection->inTransaction(), 'Transaction closed after popping the outer transaction'); + $this->assertRowPresent('outer'); + $this->assertRowPresent('inner'); + + // Rollback the inner transaction. + $this->cleanUp(); + $transaction = $this->createRootTransaction('', FALSE); + $this->insertRow('outer'); + $transaction2 = $this->createFirstSavepointTransaction('', FALSE); + $this->insertRow('inner'); + // Now rollback the inner transaction. + $transaction2->rollBack(); + $this->assertTrue($this->connection->inTransaction(), 'Still in a transaction after popping the outer transaction'); + // Pop the outer transaction, it should commit. + $this->insertRow('outer-after-inner-rollback'); + $transaction->commitOrRelease(); + $this->assertFalse($this->connection->inTransaction(), 'Transaction closed after popping the inner transaction'); + $this->assertRowPresent('outer'); + $this->assertRowAbsent('inner'); + $this->assertRowPresent('outer-after-inner-rollback'); + } + + /** + * Tests that transactions can continue to be used if a query fails. + */ + public function testQueryFailureInTransaction(): void { + $transaction = $this->createRootTransaction('test_transaction', FALSE); + $this->connection->schema()->dropTable('test'); + + // Test a failed query using the query() method. + try { + $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'David'])->fetchField(); + $this->fail('Using the query method should have failed.'); + } + catch (\Exception) { + // Just continue testing. + } + + // Test a failed select query. + try { + $this->connection->select('test') + ->fields('test', ['name']) + ->execute(); + + $this->fail('Select query should have failed.'); + } + catch (\Exception) { + // Just continue testing. + } + + // Test a failed insert query. + try { + $this->connection->insert('test') + ->fields([ + 'name' => 'David', + 'age' => '24', + ]) + ->execute(); + + $this->fail('Insert query should have failed.'); + } + catch (\Exception) { + // Just continue testing. + } + + // Test a failed update query. + try { + $this->connection->update('test') + ->fields(['name' => 'Tiffany']) + ->condition('id', 1) + ->execute(); + + $this->fail('Update query should have failed.'); + } + catch (\Exception) { + // Just continue testing. + } + + // Test a failed delete query. + try { + $this->connection->delete('test') + ->condition('id', 1) + ->execute(); + + $this->fail('Delete query should have failed.'); + } + catch (\Exception) { + // Just continue testing. + } + + // Test a failed merge query. + try { + $this->connection->merge('test') + ->key('job', 'Presenter') + ->fields([ + 'age' => '31', + 'name' => 'Tiffany', + ]) + ->execute(); + + $this->fail('Merge query should have failed.'); + } + catch (\Exception) { + // Just continue testing. + } + + // Test a failed upsert query. + try { + $this->connection->upsert('test') + ->key('job') + ->fields(['job', 'age', 'name']) + ->values([ + 'job' => 'Presenter', + 'age' => 31, + 'name' => 'Tiffany', + ]) + ->execute(); + + $this->fail('Upsert query should have failed.'); + } + catch (\Exception) { + // Just continue testing. + } + + // Create the missing schema and insert a row. + $this->installSchema('database_test', ['test']); + $this->connection->insert('test') + ->fields([ + 'name' => 'David', + 'age' => '24', + ]) + ->execute(); + + // Commit the transaction. + if ($this->connection->supportsTransactionalDDL()) { + $transaction->commitOrRelease(); + } + else { + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction->commitOrRelease(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::commitOrRelease() was not processed because a prior execution of a DDL statement already committed the transaction.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + } + + $saved_age = $this->connection->query('SELECT [age] FROM {test} WHERE [name] = :name', [':name' => 'David'])->fetchField(); + $this->assertEquals('24', $saved_age); + } + + /** + * Tests releasing a savepoint before last is safe. + */ + public function testReleaseIntermediateSavepoint(): void { + $transaction = $this->createRootTransaction(); + $savepoint1 = $this->createFirstSavepointTransaction('', FALSE); + + // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_2' + // on the database. + $savepoint2 = $this->connection->startTransaction(); + $this->assertSame(3, $this->connection->transactionManager()->stackDepth()); + // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_3' + // on the database. + // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis + $savepoint3 = $this->connection->startTransaction(); + $this->assertSame(4, $this->connection->transactionManager()->stackDepth()); + // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_4' + // on the database. + // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis + $savepoint4 = $this->connection->startTransaction(); + $this->assertSame(5, $this->connection->transactionManager()->stackDepth()); + + $this->insertRow('row'); + + // Release savepoint transaction. Corresponds to 'RELEASE SAVEPOINT + // savepoint_2' on the database. + $savepoint2->commitOrRelease(); + // Since we have committed an intermediate savepoint Transaction object, + // the savepoints created later have been dropped by the database already. + $this->assertSame(2, $this->connection->transactionManager()->stackDepth()); + $this->assertRowPresent('row'); + + // Commit the remaining Transaction objects. The client transaction is + // eventually committed. + $savepoint1->commitOrRelease(); + $transaction->commitOrRelease(); + $this->assertFalse($this->connection->inTransaction()); + $this->assertRowPresent('row'); + } + + /** + * Tests committing a transaction while savepoints are active. + */ + public function testCommitWithActiveSavepoint(): void { + $transaction = $this->createRootTransaction(); + // phpcs:ignore DrupalPractice.CodeAnalysis.VariableAnalysis + $savepoint1 = $this->createFirstSavepointTransaction('', FALSE); + + // Starts a savepoint transaction. Corresponds to 'SAVEPOINT savepoint_2' + // on the database. + $savepoint2 = $this->connection->startTransaction(); + $this->assertSame(3, $this->connection->transactionManager()->stackDepth()); + + $this->insertRow('row'); + + // Commit the root transaction. + $transaction->commitOrRelease(); + // Since we have committed the outer (root) Transaction object, the inner + // (savepoint) ones have been dropped by the database already, and we are + // no longer in an active transaction state. + $this->assertSame(0, $this->connection->transactionManager()->stackDepth()); + $this->assertFalse($this->connection->inTransaction()); + $this->assertRowPresent('row'); + // Trying to release the inner (savepoint) Transaction object, throws an + // exception since it was dropped by the database already, and removed from + // our transaction stack. + $this->expectException(TransactionOutOfOrderException::class); + $this->expectExceptionMessageMatches("/^Error attempting commit of .*\\\\savepoint_2\\. Active stack: .* empty/"); + $savepoint2->commitOrRelease(); + } + + /** + * Tests for transaction names. + */ + public function testTransactionName(): void { + $transaction = $this->createRootTransaction('', FALSE); + $this->assertSame('drupal_transaction', $transaction->name()); + + $savepoint1 = $this->createFirstSavepointTransaction('', FALSE); + $this->assertSame('savepoint_1', $savepoint1->name()); + + $this->expectException(TransactionNameNonUniqueException::class); + $this->expectExceptionMessage("savepoint_1 is already in use."); + $this->connection->startTransaction('savepoint_1'); + } + + /** + * Tests for arbitrary transaction names. + */ + public function testArbitraryTransactionNames(): void { + $transaction = $this->createRootTransaction('TinkyWinky', FALSE); + // Despite setting a name, the root transaction is always named + // 'drupal_transaction'. + $this->assertSame('drupal_transaction', $transaction->name()); + + $savepoint1 = $this->createFirstSavepointTransaction('Dipsy', FALSE); + $this->assertSame('Dipsy', $savepoint1->name()); + + $this->expectException(TransactionNameNonUniqueException::class); + $this->expectExceptionMessage("Dipsy is already in use."); + $this->connection->startTransaction('Dipsy'); + } + + /** + * Tests that adding a post-transaction callback fails with no transaction. + */ + public function testRootTransactionEndCallbackAddedWithoutTransaction(): void { + $this->expectException(\LogicException::class); + $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']); + } + + /** + * Tests post-transaction callback executes after transaction commit. + */ + public function testRootTransactionEndCallbackCalledOnCommit(): void { + $transaction = $this->createRootTransaction('', FALSE); + $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']); + $this->insertRow('row'); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + + // Callbacks are processed only when destructing the transaction. + // Executing a commit is not sufficient by itself. + $transaction->commitOrRelease(); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowPresent('row'); + $this->assertRowAbsent('rtcCommit'); + + // Destruct the transaction. + unset($transaction); + + // The post-transaction callback should now have inserted a 'rtcCommit' + // row. + $this->assertSame('rtcCommit', $this->postTransactionCallbackAction); + $this->assertRowPresent('row'); + $this->assertRowPresent('rtcCommit'); + } + + /** + * Tests post-transaction callback executes after transaction rollback. + */ + public function testRootTransactionEndCallbackCalledAfterRollbackAndDestruction(): void { + $transaction = $this->createRootTransaction('', FALSE); + $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']); + $this->insertRow('row'); + $this->assertNull($this->postTransactionCallbackAction); + + // Callbacks are processed only when destructing the transaction. + // Executing a rollback is not sufficient by itself. + $transaction->rollBack(); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowAbsent('rtcRollback'); + $this->assertRowAbsent('row'); + + // Destruct the transaction. + unset($transaction); + + // The post-transaction callback should now have inserted a 'rtcRollback' + // row. + $this->assertSame('rtcRollback', $this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowPresent('rtcRollback'); + $this->assertRowAbsent('row'); + } + + /** + * Tests post-transaction callback executes after a DDL statement. + */ + public function testRootTransactionEndCallbackCalledAfterDdlAndDestruction(): void { + $transaction = $this->createRootTransaction('', FALSE); + $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']); + $this->insertRow('row'); + $this->assertNull($this->postTransactionCallbackAction); + + // Callbacks are processed only when destructing the transaction. + // Executing a DDL statement is not sufficient itself. + // We cannot use truncate here, since it has protective code to fall back + // to a transactional delete when in transaction. We drop an unrelated + // table instead. + $this->connection->schema()->dropTable('test_people'); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowAbsent('rtcRollback'); + $this->assertRowPresent('row'); + + // Destruct the transaction. + unset($transaction); + + // The post-transaction callback should now have inserted a 'rtcCommit' + // row. + $this->assertSame('rtcCommit', $this->postTransactionCallbackAction); + $this->assertRowPresent('rtcCommit'); + $this->assertRowAbsent('rtcRollback'); + $this->assertRowPresent('row'); + } + + /** + * Tests post-transaction rollback executes after a DDL statement. + * + * For database servers that support transactional DDL, a rollback of a + * transaction including DDL statements is possible. + */ + public function testRootTransactionEndCallbackCalledAfterDdlAndRollbackForTransactionalDdlDatabase(): void { + if (!$this->connection->supportsTransactionalDDL()) { + $this->markTestSkipped('This test only works for database supporting transactional DDL.'); + } + + $transaction = $this->createRootTransaction('', FALSE); + $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']); + $this->insertRow('row'); + $this->assertNull($this->postTransactionCallbackAction); + + // Callbacks are processed only when destructing the transaction. + // Executing a DDL statement is not sufficient itself. + // We cannot use truncate here, since it has protective code to fall back + // to a transactional delete when in transaction. We drop an unrelated + // table instead. + $this->connection->schema()->dropTable('test_people'); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowAbsent('rtcRollback'); + $this->assertRowPresent('row'); + + // Callbacks are processed only when destructing the transaction. + // Executing the rollback is not sufficient by itself. + $transaction->rollBack(); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowAbsent('rtcRollback'); + $this->assertRowAbsent('row'); + + // Destruct the transaction. + unset($transaction); + + // The post-transaction callback should now have inserted a 'rtcRollback' + // row. + $this->assertSame('rtcRollback', $this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowPresent('rtcRollback'); + $this->assertRowAbsent('row'); + } + + /** + * Tests post-transaction rollback failure after a DDL statement. + * + * For database servers that support transactional DDL, a rollback of a + * transaction including DDL statements is not possible, since a commit + * happened already. We cannot decide what should be the status of the + * callback, an exception is thrown. + */ + public function testRootTransactionEndCallbackFailureUponDdlAndRollbackForNonTransactionalDdlDatabase(): void { + if ($this->connection->supportsTransactionalDDL()) { + $this->markTestSkipped('This test only works for database that do not support transactional DDL.'); + } + + $transaction = $this->createRootTransaction('', FALSE); + $this->connection->transactionManager()->addPostTransactionCallback([$this, 'rootTransactionCallback']); + $this->insertRow('row'); + $this->assertNull($this->postTransactionCallbackAction); + + // Callbacks are processed only when destructing the transaction. + // Executing a DDL statement is not sufficient itself. + // We cannot use truncate here, since it has protective code to fall back + // to a transactional delete when in transaction. We drop an unrelated + // table instead. + $this->connection->schema()->dropTable('test_people'); + $this->assertNull($this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowAbsent('rtcRollback'); + $this->assertRowPresent('row'); + + set_error_handler(static function (int $errno, string $errstr): bool { + throw new \ErrorException($errstr); + }); + try { + $transaction->rollBack(); + } + catch (\ErrorException $e) { + $this->assertSame('Transaction::rollBack() failed because of a prior execution of a DDL statement.', $e->getMessage()); + } + finally { + restore_error_handler(); + } + + unset($transaction); + + // The post-transaction callback should now have inserted a 'rtcRollback' + // row. + $this->assertSame('rtcRollback', $this->postTransactionCallbackAction); + $this->assertRowAbsent('rtcCommit'); + $this->assertRowPresent('rtcRollback'); + $manager = $this->connection->transactionManager(); + $this->assertSame(0, $manager->stackDepth()); + $reflectedTransactionState = new \ReflectionMethod($manager, 'getConnectionTransactionState'); + $this->assertSame(ClientConnectionTransactionState::RollbackFailed, $reflectedTransactionState->invoke($manager)); + $this->assertRowPresent('row'); + } + + /** + * A post-transaction callback for testing purposes. + */ + public function rootTransactionCallback(bool $success): void { + $this->postTransactionCallbackAction = $success ? 'rtcCommit' : 'rtcRollback'; + $this->insertRow($this->postTransactionCallbackAction); + } + + /** + * Tests TransactionManager failure. + */ + public function testTransactionManagerFailureOnPendingStackItems(): void { + $connectionInfo = Database::getConnectionInfo(); + Database::addConnectionInfo('default', 'test_fail', $connectionInfo['default']); + $testConnection = Database::getConnection('test_fail'); + + // Add a fake item to the stack. + $manager = $testConnection->transactionManager(); + $reflectionMethod = new \ReflectionMethod($manager, 'addStackItem'); + $reflectionMethod->invoke($manager, 'bar', new StackItem('qux', StackItemType::Root)); + // Ensure transaction state can be determined during object destruction. + // This is necessary for the test to pass when xdebug.mode has the 'develop' + // option enabled. + $reflectionProperty = new \ReflectionProperty(TransactionManagerBase::class, 'connectionTransactionState'); + $reflectionProperty->setValue($manager, ClientConnectionTransactionState::Active); + + // Ensure that __destruct() results in an assertion error. Note that this + // will normally be called by PHP during the object's destruction but Drupal + // will commit all transactions when a database is closed thereby making + // this impossible to test unless it is called directly. + try { + $manager->__destruct(); + $this->fail("Expected AssertionError error not thrown"); + } + catch (\AssertionError $e) { + $this->assertStringStartsWith('Transaction $stack was not empty. Active stack: bar\\qux', $e->getMessage()); + } + + // Clean up. + $reflectionProperty = new \ReflectionProperty(TransactionManagerBase::class, 'stack'); + $reflectionProperty->setValue($manager, []); + unset($testConnection); + Database::closeConnection('test_fail'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Datetime/DrupalDateTimeTest.php b/core/tests/Drupal/KernelTests/Core/Datetime/DrupalDateTimeTest.php new file mode 100644 index 000000000000..e1542a30c381 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Datetime/DrupalDateTimeTest.php @@ -0,0 +1,109 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Datetime; + +use Drupal\Core\Datetime\DrupalDateTime; +use Drupal\KernelTests\KernelTestBase; +use Drupal\Tests\user\Traits\UserCreationTrait; + +/** + * Tests DrupalDateTime functionality. + * + * @group Datetime + */ +class DrupalDateTimeTest extends KernelTestBase { + + use UserCreationTrait; + + /** + * Set up required modules. + * + * @var string[] + */ + protected static $modules = [ + 'system', + 'user', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installConfig(['system']); + $this->installEntitySchema('user'); + } + + /** + * Tests that DrupalDateTime can detect the right timezone to use. + * + * Test with a variety of less commonly used timezone names to + * help ensure that the system timezone will be different than the + * stated timezones. + */ + public function testDateTimezone(): void { + $date_string = '2007-01-31 21:00:00'; + + // Make sure no site timezone has been set. + $this->config('system.date') + ->set('timezone.user.configurable', 0) + ->set('timezone.default', NULL) + ->save(); + + // Detect the system timezone. + $system_timezone = date_default_timezone_get(); + + // Create a date object with an unspecified timezone, which should + // end up using the system timezone. + $date = new DrupalDateTime($date_string); + $timezone = $date->getTimezone()->getName(); + $this->assertSame($system_timezone, $timezone, 'DrupalDateTime uses the system timezone when there is no site timezone.'); + + // Create a date object with a specified timezone. + $date = new DrupalDateTime($date_string, 'America/Yellowknife'); + $timezone = $date->getTimezone()->getName(); + $this->assertSame('America/Yellowknife', $timezone, 'DrupalDateTime uses the specified timezone if provided.'); + + // Set a site timezone. + $this->config('system.date')->set('timezone.default', 'Europe/Warsaw')->save(); + + // Create a date object with an unspecified timezone, which should + // end up using the site timezone. + $date = new DrupalDateTime($date_string); + $timezone = $date->getTimezone()->getName(); + $this->assertSame('Europe/Warsaw', $timezone, 'DrupalDateTime uses the site timezone if provided.'); + + // Create user. + $this->config('system.date')->set('timezone.user.configurable', 1)->save(); + $this->setUpCurrentUser([ + 'timezone' => 'Asia/Manila', + ]); + + // Create a date object with an unspecified timezone, which should + // end up using the user timezone. + $date = new DrupalDateTime($date_string); + $timezone = $date->getTimezone()->getName(); + $this->assertSame('Asia/Manila', $timezone, 'DrupalDateTime uses the user timezone, if configurable timezones are used and it is set.'); + } + + /** + * Tests the ability to override the time zone in the format method. + */ + public function testTimezoneFormat(): void { + // Create a date in UTC + $date = DrupalDateTime::createFromTimestamp(87654321, 'UTC'); + + // Verify that the date format method displays the default time zone. + $this->assertEquals('1972/10/11 12:25:21 UTC', $date->format('Y/m/d H:i:s e'), 'Date has default UTC time zone and correct date/time.'); + + // Verify that the format method can override the time zone. + $this->assertEquals('1972/10/11 08:25:21 America/New_York', $date->format('Y/m/d H:i:s e', ['timezone' => 'America/New_York']), 'Date displayed overridden time zone and correct date/time'); + + // Verify that the date format method still displays the default time zone + // for the date object. + $this->assertEquals('1972/10/11 12:25:21 UTC', $date->format('Y/m/d H:i:s e'), 'Date still has default UTC time zone and correct date/time'); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php b/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php index 3be9b023b2f7..54d72e9fbff5 100644 --- a/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php +++ b/core/tests/Drupal/KernelTests/Core/DrupalKernel/DrupalKernelTest.php @@ -7,7 +7,6 @@ namespace Drupal\KernelTests\Core\DrupalKernel; use Composer\Autoload\ClassLoader; use Drupal\Core\DrupalKernel; use Drupal\Core\DrupalKernelInterface; -use Drupal\Core\Utility\Error; use Drupal\KernelTests\KernelTestBase; use org\bovigo\vfs\vfsStream; use Prophecy\Argument; @@ -27,8 +26,7 @@ class DrupalKernelTest extends KernelTestBase { * {@inheritdoc} */ protected function tearDown(): void { - $currentErrorHandler = Error::currentErrorHandler(); - if (is_string($currentErrorHandler) && $currentErrorHandler === '_drupal_error_handler') { + if (get_error_handler() === '_drupal_error_handler') { restore_error_handler(); } parent::tearDown(); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleEntityTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleEntityTest.php new file mode 100644 index 000000000000..419d8bff4d43 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityBundleEntityTest.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Entity; + +use Drupal\entity_test\Entity\EntityTest; +use Drupal\entity_test\Entity\EntityTestBundle; +use Drupal\entity_test\Entity\EntityTestNoBundleWithLabel; +use Drupal\entity_test\Entity\EntityTestWithBundle; + +/** + * Tests the getBundleEntity() method. + * + * @coversDefaultClass \Drupal\Core\Entity\ContentEntityBase + * + * @group Entity + */ +class EntityBundleEntityTest extends EntityKernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['entity_test']; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('entity_test_with_bundle'); + $this->installEntitySchema('entity_test_no_bundle_with_label'); + } + + /** + * Tests an entity type with config entities for bundles. + * + * @covers ::getBundleEntity + */ + public function testWithConfigBundleEntity(): void { + $bundleEntity = EntityTestBundle::create([ + 'id' => 'bundle_alpha', + 'label' => 'Alpha', + ]); + $bundleEntity->save(); + + $entity = EntityTestWithBundle::create([ + 'type' => 'bundle_alpha', + 'name' => 'foo', + ]); + $entity->save(); + $this->assertEquals($bundleEntity->id(), $entity->getBundleEntity()->id()); + } + + /** + * Tests an entity type without config entities for bundles. + * + * EntityTest doesn't have bundles, but does have the bundle entity key. + * + * @covers ::getBundleEntity + */ + public function testWithoutBundleEntity(): void { + $entity = EntityTest::create([ + 'name' => 'foo', + ]); + $entity->save(); + $this->assertNull($entity->getBundleEntity()); + } + + /** + * Tests an entity type without the bundle entity key. + * + * @covers ::getBundleEntity + */ + public function testWithBundleKeyEntity(): void { + $entity = EntityTestNoBundleWithLabel::create([ + 'name' => 'foo', + ]); + $entity->save(); + $this->assertNull($entity->getBundleEntity()); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php new file mode 100644 index 000000000000..a914752e9f17 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php @@ -0,0 +1,1036 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Entity; + +use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\Database\Database; +use Drupal\Core\Database\IntegrityConstraintViolationException; +use Drupal\Core\Entity\ContentEntityType; +use Drupal\Core\Entity\EntityStorageException; +use Drupal\Core\Entity\EntityTypeEvents; +use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; +use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Field\FieldException; +use Drupal\Core\Field\FieldStorageDefinitionEvents; +use Drupal\Core\Language\LanguageInterface; +use Drupal\Core\StringTranslation\TranslatableMarkup; +use Drupal\entity_test\EntityTestHelper; +use Drupal\entity_test\FieldStorageDefinition; +use Drupal\entity_test_update\Entity\EntityTestUpdate; +use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait; + +/** + * Tests EntityDefinitionUpdateManager functionality. + * + * @coversDefaultClass \Drupal\Core\Entity\EntityDefinitionUpdateManager + * + * @group Entity + * @group #slow + */ +class EntityDefinitionUpdateMultipleTypesTest extends EntityKernelTestBase { + + use EntityDefinitionTestTrait; + + /** + * The entity definition update manager. + * + * @var \Drupal\Core\Entity\EntityDefinitionUpdateManagerInterface + */ + protected $entityDefinitionUpdateManager; + + /** + * The entity field manager. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface + */ + protected $entityFieldManager; + + /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * {@inheritdoc} + */ + protected static $modules = ['entity_test_update', 'language']; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->entityDefinitionUpdateManager = $this->container->get('entity.definition_update_manager'); + $this->entityFieldManager = $this->container->get('entity_field.manager'); + $this->database = $this->container->get('database'); + + // Install every entity type's schema that wasn't installed in the parent + // method. + foreach (array_diff_key($this->entityTypeManager->getDefinitions(), array_flip(['user', 'entity_test'])) as $entity_type_id => $entity_type) { + $this->installEntitySchema($entity_type_id); + } + } + + /** + * Tests when no definition update is needed. + */ + public function testNoUpdates(): void { + // Ensure that the definition update manager reports no updates. + $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that no updates are needed.'); + $this->assertSame([], $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports an empty change summary.'); + $this->assertSame([], $this->entityDefinitionUpdateManager->getChangeList(), 'EntityDefinitionUpdateManager reports an empty change list.'); + } + + /** + * Tests updating entity schema when there are no existing entities. + */ + public function testEntityTypeUpdateWithoutData(): void { + // The 'entity_test_update' entity type starts out non-revisionable, so + // ensure the revision table hasn't been created during setUp(). + $this->assertFalse($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table not created for entity_test_update.'); + + // Update it to be revisionable and ensure the definition update manager + // reports that an update is needed. + $this->updateEntityTypeToRevisionable(); + $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); + $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel(); + $expected = [ + 'entity_test_update' => [ + "The $entity_type entity type needs to be updated.", + // The revision key is now defined, so the revision field needs to be + // created. + 'The Revision ID field needs to be installed.', + 'The Default revision field needs to be installed.', + ], + ]; + $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.'); + + // Run the update and ensure the revision table is created. + $this->updateEntityTypeToRevisionable(TRUE); + $this->assertTrue($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table created for entity_test_update.'); + } + + /** + * Tests updating entity schema when there are entity storage changes. + */ + public function testEntityTypeUpdateWithEntityStorageChange(): void { + // Update the entity type to be revisionable and try to apply the update. + // It's expected to throw an exception. + $entity_type = $this->getUpdatedEntityTypeDefinition(TRUE, FALSE); + try { + $this->entityDefinitionUpdateManager->updateEntityType($entity_type); + $this->fail('EntityStorageException thrown when trying to apply an update that requires shared table schema changes.'); + } + catch (EntityStorageException) { + // Expected exception; just continue testing. + } + } + + /** + * Tests creating a fieldable entity type that doesn't exist in code anymore. + * + * @covers ::installFieldableEntityType + */ + public function testInstallFieldableEntityTypeWithoutInCodeDefinition(): void { + $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update'); + $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('entity_test_update'); + + // Remove the entity type definition. This is the same thing as removing the + // code that defines it. + $this->deleteEntityType(); + + // Install the entity type and check that its tables have been created. + $this->entityDefinitionUpdateManager->installFieldableEntityType($entity_type, $field_storage_definitions); + $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'The base table of the entity type has been created.'); + } + + /** + * Tests updating an entity type that doesn't exist in code anymore. + * + * @covers ::updateEntityType + */ + public function testUpdateEntityTypeWithoutInCodeDefinition(): void { + $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update'); + + // Remove the entity type definition. This is the same thing as removing the + // code that defines it. + $this->deleteEntityType(); + + // Add an entity index, update the entity type and check that the index has + // been created. + $this->addEntityIndex(); + $this->entityDefinitionUpdateManager->updateEntityType($entity_type); + + $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.'); + } + + /** + * Tests updating a fieldable entity type that doesn't exist in code anymore. + * + * @covers ::updateFieldableEntityType + */ + public function testUpdateFieldableEntityTypeWithoutInCodeDefinition(): void { + $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update'); + $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('entity_test_update'); + + // Remove the entity type definition. This is the same thing as removing the + // code that defines it. + $this->deleteEntityType(); + + // Rename the base table, update the fieldable entity type and check that + // the table has been renamed. + $entity_type->set('base_table', 'entity_test_update_new'); + $this->entityDefinitionUpdateManager->updateFieldableEntityType($entity_type, $field_storage_definitions); + + $this->assertTrue($this->database->schema()->tableExists('entity_test_update_new'), 'The base table has been renamed.'); + $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'The old base table does not exist anymore.'); + } + + /** + * Tests uninstalling an entity type that doesn't exist in code anymore. + * + * @covers ::uninstallEntityType + */ + public function testUninstallEntityTypeWithoutInCodeDefinition(): void { + $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update'); + + // Remove the entity type definition. This is the same thing as removing the + // code that defines it. + $this->deleteEntityType(); + + // Now uninstall it and check that the tables have been removed. + $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update exists before uninstalling it.'); + $this->entityDefinitionUpdateManager->uninstallEntityType($entity_type); + $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update does not exist anymore.'); + } + + /** + * Tests uninstalling a revisionable entity type that doesn't exist in code. + * + * @covers ::uninstallEntityType + */ + public function testUninstallRevisionableEntityTypeWithoutInCodeDefinition(): void { + $this->updateEntityTypeToRevisionable(TRUE); + $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update'); + + // Remove the entity type definition. This is the same thing as removing the + // code that defines it. + $this->deleteEntityType(); + + // Now uninstall it and check that the tables have been removed. + $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update exists before uninstalling it.'); + $this->entityDefinitionUpdateManager->uninstallEntityType($entity_type); + $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update does not exist anymore.'); + } + + /** + * Tests creating, updating, and deleting a base field if no entities exist. + */ + public function testBaseFieldCreateUpdateDeleteWithoutData(): void { + // Add a base field, ensure the update manager reports it, and the update + // creates its schema. + $this->addBaseField(); + $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); + $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); + $this->assertCount(1, $changes['entity_test_update']); + $this->assertEquals('The A new base field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0])); + $this->applyEntityUpdates(); + $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.'); + + // Add an index on the base field, ensure the update manager reports it, + // and the update creates it. + $this->addBaseFieldIndex(); + $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); + $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); + $this->assertCount(1, $changes['entity_test_update']); + $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0])); + $this->applyEntityUpdates(); + $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index created.'); + + // Remove the above index, ensure the update manager reports it, and the + // update deletes it. + $this->removeBaseFieldIndex(); + $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); + $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); + $this->assertCount(1, $changes['entity_test_update']); + $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0])); + $this->applyEntityUpdates(); + $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index deleted.'); + + // Update the type of the base field from 'string' to 'text', ensure the + // update manager reports it, and the update adjusts the schema + // accordingly. + $this->modifyBaseField(); + $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); + $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); + $this->assertCount(1, $changes['entity_test_update']); + $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0])); + $this->applyEntityUpdates(); + $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Original column deleted in shared table for new_base_field.'); + $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__value'), 'Value column created in shared table for new_base_field.'); + $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__format'), 'Format column created in shared table for new_base_field.'); + + // Remove the base field, ensure the update manager reports it, and the + // update deletes the schema. + $this->removeBaseField(); + $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); + $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); + $this->assertCount(1, $changes['entity_test_update']); + $this->assertEquals('The A new base field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0])); + $this->applyEntityUpdates(); + $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_value'), 'Value column deleted from shared table for new_base_field.'); + $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_format'), 'Format column deleted from shared table for new_base_field.'); + } + + /** + * Tests creating, updating, and deleting a base field with no label set. + * + * See testBaseFieldCreateUpdateDeleteWithoutData() for more details + */ + public function testBaseFieldWithoutLabelCreateUpdateDelete(): void { + // Add a base field, ensure the update manager reports it with the + // field id. + $this->addBaseField('string', 'entity_test_update', FALSE, FALSE); + $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); + $this->assertCount(1, $changes['entity_test_update']); + $this->assertEquals('The new_base_field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0])); + $this->applyEntityUpdates(); + + // Add an index on the base field, ensure the update manager reports it with + // the field id. + $this->addBaseFieldIndex(); + $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); + $this->assertCount(1, $changes['entity_test_update']); + $this->assertEquals('The new_base_field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0])); + $this->applyEntityUpdates(); + + // Remove the base field, ensure the update manager reports it with the + // field id. + $this->removeBaseField(); + $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); + $this->assertCount(1, $changes['entity_test_update']); + $this->assertEquals('The new_base_field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0])); + } + + /** + * Tests creating, updating, and deleting a bundle field if no entities exist. + */ + public function testBundleFieldCreateUpdateDeleteWithoutData(): void { + // Add a bundle field, ensure the update manager reports it, and the update + // creates its schema. + $this->addBundleField(); + $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); + $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); + $this->assertCount(1, $changes['entity_test_update']); + $this->assertEquals('The A new bundle field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0])); + $this->applyEntityUpdates(); + $this->assertTrue($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.'); + + // Update the type of the base field from 'string' to 'text', ensure the + // update manager reports it, and the update adjusts the schema + // accordingly. + $this->modifyBundleField(); + $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); + $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); + $this->assertCount(1, $changes['entity_test_update']); + $this->assertEquals('The A new bundle field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0])); + $this->applyEntityUpdates(); + $this->assertTrue($this->database->schema()->fieldExists('entity_test_update__new_bundle_field', 'new_bundle_field_format'), 'Format column created in dedicated table for new_base_field.'); + + // Remove the bundle field, ensure the update manager reports it, and the + // update deletes the schema. + $this->removeBundleField(); + $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); + $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); + $this->assertCount(1, $changes['entity_test_update']); + $this->assertEquals('The A new bundle field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0])); + $this->applyEntityUpdates(); + $this->assertFalse($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.'); + } + + /** + * Tests creating and deleting a base field if entities exist. + * + * This tests deletion when there are existing entities, but non-existent data + * for the field being deleted. + * + * @see testBaseFieldDeleteWithExistingData() + */ + public function testBaseFieldCreateDeleteWithExistingEntities(): void { + // Save an entity. + $name = $this->randomString(); + $storage = $this->entityTypeManager->getStorage('entity_test_update'); + $entity = $storage->create(['name' => $name]); + $entity->save(); + + // Add a base field and run the update. Ensure the base field's column is + // created and the prior saved entity data is still there. + $this->addBaseField(); + $this->applyEntityUpdates(); + $schema_handler = $this->database->schema(); + $this->assertTrue($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.'); + $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id()); + $this->assertSame($name, $entity->name->value, 'Entity data preserved during field creation.'); + + // Remove the base field and run the update. Ensure the base field's column + // is deleted and the prior saved entity data is still there. + $this->removeBaseField(); + $this->applyEntityUpdates(); + $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column deleted from shared table for new_base_field.'); + $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id()); + $this->assertSame($name, $entity->name->value, 'Entity data preserved during field deletion.'); + + // Add a base field with a required property and run the update. Ensure + // 'not null' is not applied and thus no exception is thrown. + $this->addBaseField('shape_required'); + $this->applyEntityUpdates(); + $assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color'); + $this->assertTrue($assert, 'Columns created in shared table for new_base_field.'); + + // Recreate the field after emptying the base table and check that its + // columns are not 'not null'. + // @todo Revisit this test when allowing for required storage field + // definitions. See https://www.drupal.org/node/2390495. + $entity->delete(); + $this->removeBaseField(); + $this->applyEntityUpdates(); + $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field__shape'), 'Shape column should be removed from the shared table for new_base_field.'); + $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field__color'), 'Color column should be removed from the shared table for new_base_field.'); + $this->addBaseField('shape_required'); + $this->applyEntityUpdates(); + $assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color'); + $this->assertTrue($assert, 'Columns created again in shared table for new_base_field.'); + $entity = $storage->create(['name' => $name]); + $entity->save(); + } + + /** + * Tests creating and deleting a bundle field if entities exist. + * + * This tests deletion when there are existing entities, but non-existent data + * for the field being deleted. + * + * @see testBundleFieldDeleteWithExistingData() + */ + public function testBundleFieldCreateDeleteWithExistingEntities(): void { + // Save an entity. + $name = $this->randomString(); + $storage = $this->entityTypeManager->getStorage('entity_test_update'); + $entity = $storage->create(['name' => $name]); + $entity->save(); + + // Add a bundle field and run the update. Ensure the bundle field's table + // is created and the prior saved entity data is still there. + $this->addBundleField(); + $this->applyEntityUpdates(); + $schema_handler = $this->database->schema(); + $this->assertTrue($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.'); + $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id()); + $this->assertSame($name, $entity->name->value, 'Entity data preserved during field creation.'); + + // Remove the base field and run the update. Ensure the bundle field's + // table is deleted and the prior saved entity data is still there. + $this->removeBundleField(); + $this->applyEntityUpdates(); + $this->assertFalse($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.'); + $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id()); + $this->assertSame($name, $entity->name->value, 'Entity data preserved during field deletion.'); + + // Test that required columns are created as 'not null'. + $this->addBundleField('shape_required'); + $this->applyEntityUpdates(); + $message = 'The new_bundle_field_shape column is not nullable.'; + $values = [ + 'bundle' => $entity->bundle(), + 'deleted' => 0, + 'entity_id' => $entity->id(), + 'revision_id' => $entity->id(), + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + 'delta' => 0, + 'new_bundle_field_color' => $this->randomString(), + ]; + try { + // Try to insert a record without providing a value for the 'not null' + // column. This should fail. + $this->database->insert('entity_test_update__new_bundle_field') + ->fields($values) + ->execute(); + $this->fail($message); + } + catch (IntegrityConstraintViolationException) { + // Now provide a value for the 'not null' column. This is expected to + // succeed. + $values['new_bundle_field_shape'] = $this->randomString(); + $this->database->insert('entity_test_update__new_bundle_field') + ->fields($values) + ->execute(); + } + } + + /** + * Tests deleting a bundle field when it has existing data. + */ + public function testBundleFieldDeleteWithExistingData(): void { + /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */ + $storage = $this->entityTypeManager->getStorage('entity_test_update'); + $schema_handler = $this->database->schema(); + + // Add the bundle field and run the update. + $this->addBundleField(); + $this->applyEntityUpdates(); + + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $storage->getTableMapping(); + $storage_definition = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledFieldStorageDefinitions('entity_test_update')['new_bundle_field']; + + // Check that the bundle field has a dedicated table. + $dedicated_table_name = $table_mapping->getDedicatedDataTableName($storage_definition); + $this->assertTrue($schema_handler->tableExists($dedicated_table_name), 'The bundle field uses a dedicated table.'); + + // Save an entity with the bundle field populated. + EntityTestHelper::createBundle('custom'); + $entity = $storage->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo']); + $entity->save(); + + // Remove the bundle field and apply updates. + $this->removeBundleField(); + $this->applyEntityUpdates(); + + // Check that the table of the bundle field has been renamed to use a + // 'deleted' table name. + $this->assertFalse($schema_handler->tableExists($dedicated_table_name), 'The dedicated table of the bundle field no longer exists.'); + + $dedicated_deleted_table_name = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE); + $this->assertTrue($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle fields has been renamed to use the "deleted" name.'); + + // Check that the deleted field's data is preserved in the dedicated + // 'deleted' table. + $result = $this->database->select($dedicated_deleted_table_name, 't') + ->fields('t') + ->execute() + ->fetchAll(); + $this->assertCount(1, $result); + + $expected = [ + 'bundle' => $entity->bundle(), + 'deleted' => '1', + 'entity_id' => $entity->id(), + 'revision_id' => $entity->id(), + 'langcode' => $entity->language()->getId(), + 'delta' => '0', + 'new_bundle_field_value' => $entity->new_bundle_field->value, + ]; + // Use assertEquals and not assertSame here to prevent that a different + // sequence of the columns in the table will affect the check. + $this->assertEquals($expected, (array) $result[0]); + + // Check that the field definition is marked for purging. + $deleted_field_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldDefinitions(); + $this->assertArrayHasKey($storage_definition->getUniqueIdentifier(), $deleted_field_definitions, 'The bundle field is marked for purging.'); + + // Check that the field storage definition is marked for purging. + $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions(); + $this->assertArrayHasKey($storage_definition->getUniqueStorageIdentifier(), $deleted_storage_definitions, 'The bundle field storage is marked for purging.'); + + // Purge field data, and check that the storage definition has been + // completely removed once the data is purged. + field_purge_batch(10); + $deleted_field_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldDefinitions(); + $this->assertEmpty($deleted_field_definitions, 'The bundle field has been deleted.'); + $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions(); + $this->assertEmpty($deleted_storage_definitions, 'The bundle field storage has been deleted.'); + $this->assertFalse($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle field has been removed.'); + } + + /** + * Tests updating a base field when it has existing data. + */ + public function testBaseFieldUpdateWithExistingData(): void { + // Add the base field and run the update. + $this->addBaseField(); + $this->applyEntityUpdates(); + + // Save an entity with the base field populated. + $this->entityTypeManager->getStorage('entity_test_update')->create(['new_base_field' => 'foo'])->save(); + + // Change the field's field type and apply updates. It's expected to + // throw an exception. + $this->modifyBaseField(); + try { + $this->applyEntityUpdates(); + $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.'); + } + catch (FieldStorageDefinitionUpdateForbiddenException) { + // Expected exception; just continue testing. + } + } + + /** + * Tests updating a bundle field when it has existing data. + */ + public function testBundleFieldUpdateWithExistingData(): void { + // Add the bundle field and run the update. + $this->addBundleField(); + $this->applyEntityUpdates(); + + // Save an entity with the bundle field populated. + EntityTestHelper::createBundle('custom'); + $this->entityTypeManager->getStorage('entity_test_update')->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo'])->save(); + + // Change the field's field type and apply updates. It's expected to + // throw an exception. + $this->modifyBundleField(); + try { + $this->applyEntityUpdates(); + $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.'); + } + catch (FieldStorageDefinitionUpdateForbiddenException) { + // Expected exception; just continue testing. + } + } + + /** + * Tests updating a bundle field when the entity type schema has changed. + */ + public function testBundleFieldUpdateWithEntityTypeSchemaUpdate(): void { + // Add the bundle field and run the update. + $this->addBundleField(); + $this->applyEntityUpdates(); + + // Update the entity type schema to revisionable but don't run the updates + // yet. + $this->updateEntityTypeToRevisionable(); + + // Perform a no-op update on the bundle field, which should work because + // both the storage and the storage schema are using the last installed + // entity type definition. + $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + $entity_definition_update_manager->updateFieldStorageDefinition($entity_definition_update_manager->getFieldStorageDefinition('new_bundle_field', 'entity_test_update')); + } + + /** + * Tests creating and deleting a multi-field index when there are no existing entities. + */ + public function testEntityIndexCreateDeleteWithoutData(): void { + // Add an entity index and ensure the update manager reports that as an + // update to the entity type. + $this->addEntityIndex(); + $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); + $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel(); + $expected = [ + 'entity_test_update' => [ + "The $entity_type entity type needs to be updated.", + ], + ]; + $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.'); + + // Run the update and ensure the new index is created. + $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update'); + $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update'); + \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original); + $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.'); + + // Remove the index and ensure the update manager reports that as an + // update to the entity type. + $this->removeEntityIndex(); + $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); + $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel(); + $expected = [ + 'entity_test_update' => [ + "The $entity_type entity type needs to be updated.", + ], + ]; + $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.'); + + // Run the update and ensure the index is deleted. + $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update'); + $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update'); + \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original); + $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.'); + + // Test that composite indexes are handled correctly when dropping and + // re-creating one of their columns. + $this->addEntityIndex(); + $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update'); + $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update'); + \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original); + + $storage_definition = $this->entityDefinitionUpdateManager->getFieldStorageDefinition('name', 'entity_test_update'); + $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition); + $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.'); + $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition); + $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.'); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('name', 'entity_test_update', 'entity_test', $storage_definition); + $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created again.'); + } + + /** + * Tests creating a multi-field index when there are existing entities. + */ + public function testEntityIndexCreateWithData(): void { + // Save an entity. + $name = $this->randomString(); + $entity = $this->entityTypeManager->getStorage('entity_test_update')->create(['name' => $name]); + $entity->save(); + + // Add an entity index, run the update. Ensure that the index is created + // despite having data. + $this->addEntityIndex(); + $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update'); + $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update'); + \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original); + $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index added.'); + } + + /** + * Tests applying single updates. + */ + public function testSingleActionCalls(): void { + $db_schema = $this->database->schema(); + + // Ensure that a non-existing entity type cannot be installed. + $message = 'A non-existing entity type cannot be installed'; + try { + $this->entityDefinitionUpdateManager->installEntityType(new ContentEntityType(['id' => 'foo'])); + $this->fail($message); + } + catch (PluginNotFoundException) { + // Expected exception; just continue testing. + } + + // Ensure that a field cannot be installed on non-existing entity type. + $message = 'A field cannot be installed on a non-existing entity type'; + try { + $storage_definition = BaseFieldDefinition::create('string') + ->setLabel('A new revisionable base field') + ->setRevisionable(TRUE); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('bar', 'foo', 'entity_test', $storage_definition); + $this->fail($message); + } + catch (PluginNotFoundException) { + // Expected exception; just continue testing. + } + + // Ensure that installing an existing entity type is a no-op. + $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update'); + $this->entityDefinitionUpdateManager->installEntityType($entity_type); + $this->assertTrue($db_schema->tableExists('entity_test_update'), 'Installing an existing entity type is a no-op'); + + // Create a new base field. + $this->addRevisionableBaseField(); + $storage_definition = BaseFieldDefinition::create('string') + ->setLabel('A new revisionable base field') + ->setRevisionable(TRUE); + $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update."); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table."); + + // Ensure that installing an existing field is a no-op. + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), 'Installing an existing field is a no-op'); + + // Update an existing field schema. + $this->modifyBaseField(); + $storage_definition = BaseFieldDefinition::create('text') + ->setName('new_base_field') + ->setTargetEntityTypeId('entity_test_update') + ->setLabel('A new revisionable base field') + ->setRevisionable(TRUE); + $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition); + $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "Previous schema for 'new_base_field' no longer exists."); + $this->assertTrue( + $db_schema->fieldExists('entity_test_update', 'new_base_field__value') && $db_schema->fieldExists('entity_test_update', 'new_base_field__format'), + "New schema for 'new_base_field' has been created." + ); + + // Drop an existing field schema. + $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition); + $this->assertFalse( + $db_schema->fieldExists('entity_test_update', 'new_base_field__value') || $db_schema->fieldExists('entity_test_update', 'new_base_field__format'), + "The schema for 'new_base_field' has been dropped." + ); + + // Make the entity type revisionable. + $this->updateEntityTypeToRevisionable(); + $this->assertFalse($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' does not exist before applying the update."); + + $this->updateEntityTypeToRevisionable(TRUE); + $this->assertTrue($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' table has been created."); + } + + /** + * Ensures that a new field and index on a shared table are created. + * + * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::createSharedTableSchema + */ + public function testCreateFieldAndIndexOnSharedTable(): void { + $this->addBaseField(); + $this->addBaseFieldIndex(); + $this->applyEntityUpdates(); + $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table."); + $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), "New index 'entity_test_update_field__new_base_field' has been created on the 'entity_test_update' table."); + // Check index size in for MySQL. + if (in_array(Database::getConnection()->driver(), ['mysql', 'mysqli'])) { + $result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update_field__new_base_field\' and column_name = \'new_base_field\'')->fetchObject(); + $this->assertEquals(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.'); + } + } + + /** + * Ensures that a new entity level index is created when data exists. + * + * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::onEntityTypeUpdate + */ + public function testCreateIndexUsingEntityStorageSchemaWithData(): void { + // Save an entity. + $name = $this->randomString(); + $storage = $this->entityTypeManager->getStorage('entity_test_update'); + $entity = $storage->create(['name' => $name]); + $entity->save(); + + // Create an index. + $indexes = [ + 'entity_test_update__type_index' => ['type'], + ]; + $this->state->set('entity_test_update.additional_entity_indexes', $indexes); + $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update'); + $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update'); + \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original); + + $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__type_index'), "New index 'entity_test_update__type_index' has been created on the 'entity_test_update' table."); + // Check index size in for MySQL. + if (in_array(Database::getConnection()->driver(), ['mysql', 'mysqli'])) { + $result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update__type_index\' and column_name = \'type\'')->fetchObject(); + $this->assertEquals(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.'); + } + } + + /** + * Tests updating a base field when it has existing data. + */ + public function testBaseFieldEntityKeyUpdateWithExistingData(): void { + // Add the base field and run the update. + $this->addBaseField(); + $this->applyEntityUpdates(); + + // Save an entity with the base field populated. + $this->entityTypeManager->getStorage('entity_test_update')->create(['new_base_field' => $this->randomString()])->save(); + + // Save an entity with the base field not populated. + /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */ + $entity = $this->entityTypeManager->getStorage('entity_test_update')->create(); + $entity->save(); + + // Promote the base field to an entity key. This will trigger the addition + // of a NOT NULL constraint. + $this->makeBaseFieldEntityKey(); + + // Field storage CRUD operations use the last installed entity type + // definition so we need to update it before doing any other field storage + // updates. + $this->entityDefinitionUpdateManager->updateEntityType($this->state->get('entity_test_update.entity_type')); + + // Try to apply the update and verify they fail since we have a NULL value. + $message = 'An error occurs when trying to enabling NOT NULL constraints with NULL data.'; + try { + $this->applyEntityUpdates(); + $this->fail($message); + } + catch (EntityStorageException) { + // Expected exception; just continue testing. + } + + // Check that the update is correctly applied when no NULL data is left. + $entity->set('new_base_field', $this->randomString()); + $entity->save(); + $this->applyEntityUpdates(); + + // Check that the update actually applied a NOT NULL constraint. + $entity->set('new_base_field', NULL); + $message = 'The NOT NULL constraint was correctly applied.'; + try { + $entity->save(); + $this->fail($message); + } + catch (EntityStorageException) { + // Expected exception; just continue testing. + } + } + + /** + * Check that field schema is correctly handled with long-named fields. + */ + public function testLongNameFieldIndexes(): void { + $this->addLongNameBaseField(); + $entity_type_id = 'entity_test_update'; + $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); + $definitions = EntityTestUpdate::baseFieldDefinitions($entity_type); + $name = 'new_long_named_entity_reference_base_field'; + $this->entityDefinitionUpdateManager->installFieldStorageDefinition($name, $entity_type_id, 'entity_test', $definitions[$name]); + $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'Entity and field schema data are correctly detected.'); + } + + /** + * Tests adding a base field with initial values. + */ + public function testInitialValue(): void { + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update'); + $db_schema = $this->database->schema(); + + // Create two entities before adding the base field. + $storage->create()->save(); + $storage->create()->save(); + + // Add a base field with an initial value. + $this->addBaseField(); + $storage_definition = BaseFieldDefinition::create('string') + ->setLabel('A new base field') + ->setInitialValue('test value'); + + $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update."); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table."); + + // Check that the initial values have been applied. + $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update'); + $entities = $storage->loadMultiple(); + $this->assertEquals('test value', $entities[1]->get('new_base_field')->value); + $this->assertEquals('test value', $entities[2]->get('new_base_field')->value); + } + + /** + * Tests entity type and field storage definition events. + */ + public function testDefinitionEvents(): void { + /** @var \Drupal\entity_test\EntityTestDefinitionSubscriber $event_subscriber */ + $event_subscriber = $this->container->get('entity_test.definition.subscriber'); + $event_subscriber->enableEventTracking(); + $event_subscriber->enableLiveDefinitionUpdates(); + + // Test field storage definition events. + $storage_definition = FieldStorageDefinition::create('string') + ->setName('field_storage_test') + ->setLabel(new TranslatableMarkup('Field storage test')) + ->setTargetEntityTypeId('entity_test_rev'); + + $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create was not dispatched yet.'); + \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionCreate($storage_definition); + $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create event successfully dispatched.'); + $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::CREATE), 'Last installed field storage definition was created before the event was fired.'); + + // Check that the newly added field can be retrieved from the live field + // storage definitions. + $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev'); + $this->assertArrayHasKey('field_storage_test', $field_storage_definitions); + + $updated_storage_definition = clone $storage_definition; + $updated_storage_definition->setLabel(new TranslatableMarkup('Updated field storage test')); + $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update was not dispatched yet.'); + \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionUpdate($updated_storage_definition, $storage_definition); + $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update event successfully dispatched.'); + $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::UPDATE), 'Last installed field storage definition was updated before the event was fired.'); + + // Check that the updated field can be retrieved from the live field storage + // definitions. + $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev'); + $this->assertEquals(new TranslatableMarkup('Updated field storage test'), $field_storage_definitions['field_storage_test']->getLabel()); + + $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete was not dispatched yet.'); + \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionDelete($storage_definition); + $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete event successfully dispatched.'); + $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::DELETE), 'Last installed field storage definition was deleted before the event was fired.'); + + // Check that the deleted field can no longer be retrieved from the live + // field storage definitions. + $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev'); + $this->assertArrayNotHasKey('field_storage_test', $field_storage_definitions); + + // Test entity type events. + $entity_type = $this->entityTypeManager->getDefinition('entity_test_rev'); + + $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create was not dispatched yet.'); + \Drupal::service('entity_type.listener')->onEntityTypeCreate($entity_type); + $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create event successfully dispatched.'); + $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::CREATE), 'Last installed entity type definition was created before the event was fired.'); + + $updated_entity_type = clone $entity_type; + $updated_entity_type->set('label', new TranslatableMarkup('Updated entity test rev')); + $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update was not dispatched yet.'); + \Drupal::service('entity_type.listener')->onEntityTypeUpdate($updated_entity_type, $entity_type); + $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update event successfully dispatched.'); + $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::UPDATE), 'Last installed entity type definition was updated before the event was fired.'); + + // Check that the updated definition can be retrieved from the live entity + // type definitions. + $entity_type = $this->entityTypeManager->getDefinition('entity_test_rev'); + $this->assertEquals(new TranslatableMarkup('Updated entity test rev'), $entity_type->getLabel()); + + $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete was not dispatched yet.'); + \Drupal::service('entity_type.listener')->onEntityTypeDelete($entity_type); + $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete event successfully dispatched.'); + $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::DELETE), 'Last installed entity type definition was deleted before the event was fired.'); + + // Check that the deleted entity type can no longer be retrieved from the + // live entity type definitions. + $this->assertNull($this->entityTypeManager->getDefinition('entity_test_rev', FALSE)); + } + + /** + * Tests the error handling when using initial values from another field. + */ + public function testInitialValueFromFieldErrorHandling(): void { + // Check that setting invalid values for 'initial value from field' doesn't + // work. + try { + $this->addBaseField(); + $storage_definition = BaseFieldDefinition::create('string') + ->setLabel('A new base field') + ->setInitialValueFromField('field_that_does_not_exist'); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->fail('Using a non-existent field as initial value does not work.'); + } + catch (FieldException $e) { + $this->assertEquals('Illegal initial value definition on new_base_field: The field field_that_does_not_exist does not exist.', $e->getMessage()); + } + + try { + $this->addBaseField(); + $storage_definition = BaseFieldDefinition::create('integer') + ->setLabel('A new base field') + ->setInitialValueFromField('name'); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); + $this->fail('Using a field of a different type as initial value does not work.'); + } + catch (FieldException $e) { + $this->assertEquals('Illegal initial value definition on new_base_field: The field types do not match.', $e->getMessage()); + } + + try { + // Add a base field that will not be stored in the shared tables. + $initial_field = BaseFieldDefinition::create('string') + ->setName('initial_field') + ->setLabel('An initial field') + ->setCardinality(2); + $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field]); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('initial_field', 'entity_test_update', 'entity_test', $initial_field); + + // Now add the base field which will try to use the previously added field + // as the source of its initial values. + $new_base_field = BaseFieldDefinition::create('string') + ->setName('new_base_field') + ->setLabel('A new base field') + ->setInitialValueFromField('initial_field'); + $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field, 'new_base_field' => $new_base_field]); + $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $new_base_field); + $this->fail('Using a field that is not stored in the shared tables as initial value does not work.'); + } + catch (FieldException $e) { + $this->assertEquals('Illegal initial value definition on new_base_field: Both fields have to be stored in the shared entity tables.', $e->getMessage()); + } + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php index 6437e594ee8b..d184892a976a 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php @@ -4,21 +4,6 @@ declare(strict_types=1); namespace Drupal\KernelTests\Core\Entity; -use Drupal\Component\Plugin\Exception\PluginNotFoundException; -use Drupal\Core\Database\Database; -use Drupal\Core\Database\IntegrityConstraintViolationException; -use Drupal\Core\Entity\ContentEntityType; -use Drupal\Core\Entity\EntityStorageException; -use Drupal\Core\Entity\EntityTypeEvents; -use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException; -use Drupal\Core\Field\BaseFieldDefinition; -use Drupal\Core\Field\FieldException; -use Drupal\Core\Field\FieldStorageDefinitionEvents; -use Drupal\Core\Language\LanguageInterface; -use Drupal\Core\StringTranslation\TranslatableMarkup; -use Drupal\entity_test\EntityTestHelper; -use Drupal\entity_test\FieldStorageDefinition; -use Drupal\entity_test_update\Entity\EntityTestUpdate; use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait; /** @@ -27,7 +12,6 @@ use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait; * @coversDefaultClass \Drupal\Core\Entity\EntityDefinitionUpdateManager * * @group Entity - * @group #slow */ class EntityDefinitionUpdateTest extends EntityKernelTestBase { @@ -67,12 +51,6 @@ class EntityDefinitionUpdateTest extends EntityKernelTestBase { $this->entityDefinitionUpdateManager = $this->container->get('entity.definition_update_manager'); $this->entityFieldManager = $this->container->get('entity_field.manager'); $this->database = $this->container->get('database'); - - // Install every entity type's schema that wasn't installed in the parent - // method. - foreach (array_diff_key($this->entityTypeManager->getDefinitions(), array_flip(['user', 'entity_test'])) as $entity_type_id => $entity_type) { - $this->installEntitySchema($entity_type_id); - } } /** @@ -96,61 +74,6 @@ class EntityDefinitionUpdateTest extends EntityKernelTestBase { } /** - * Tests when no definition update is needed. - */ - public function testNoUpdates(): void { - // Ensure that the definition update manager reports no updates. - $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that no updates are needed.'); - $this->assertSame([], $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports an empty change summary.'); - $this->assertSame([], $this->entityDefinitionUpdateManager->getChangeList(), 'EntityDefinitionUpdateManager reports an empty change list.'); - } - - /** - * Tests updating entity schema when there are no existing entities. - */ - public function testEntityTypeUpdateWithoutData(): void { - // The 'entity_test_update' entity type starts out non-revisionable, so - // ensure the revision table hasn't been created during setUp(). - $this->assertFalse($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table not created for entity_test_update.'); - - // Update it to be revisionable and ensure the definition update manager - // reports that an update is needed. - $this->updateEntityTypeToRevisionable(); - $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); - $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel(); - $expected = [ - 'entity_test_update' => [ - "The $entity_type entity type needs to be updated.", - // The revision key is now defined, so the revision field needs to be - // created. - 'The Revision ID field needs to be installed.', - 'The Default revision field needs to be installed.', - ], - ]; - $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.'); - - // Run the update and ensure the revision table is created. - $this->updateEntityTypeToRevisionable(TRUE); - $this->assertTrue($this->database->schema()->tableExists('entity_test_update_revision'), 'Revision table created for entity_test_update.'); - } - - /** - * Tests updating entity schema when there are entity storage changes. - */ - public function testEntityTypeUpdateWithEntityStorageChange(): void { - // Update the entity type to be revisionable and try to apply the update. - // It's expected to throw an exception. - $entity_type = $this->getUpdatedEntityTypeDefinition(TRUE, FALSE); - try { - $this->entityDefinitionUpdateManager->updateEntityType($entity_type); - $this->fail('EntityStorageException thrown when trying to apply an update that requires shared table schema changes.'); - } - catch (EntityStorageException) { - // Expected exception; just continue testing. - } - } - - /** * Tests installing an additional base field while installing an entity type. * * @covers ::installFieldableEntityType @@ -173,909 +96,6 @@ class EntityDefinitionUpdateTest extends EntityKernelTestBase { } /** - * Tests creating a fieldable entity type that doesn't exist in code anymore. - * - * @covers ::installFieldableEntityType - */ - public function testInstallFieldableEntityTypeWithoutInCodeDefinition(): void { - $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update'); - $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('entity_test_update'); - - // Remove the entity type definition. This is the same thing as removing the - // code that defines it. - $this->deleteEntityType(); - - // Install the entity type and check that its tables have been created. - $this->entityDefinitionUpdateManager->installFieldableEntityType($entity_type, $field_storage_definitions); - $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'The base table of the entity type has been created.'); - } - - /** - * Tests updating an entity type that doesn't exist in code anymore. - * - * @covers ::updateEntityType - */ - public function testUpdateEntityTypeWithoutInCodeDefinition(): void { - $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update'); - - // Remove the entity type definition. This is the same thing as removing the - // code that defines it. - $this->deleteEntityType(); - - // Add an entity index, update the entity type and check that the index has - // been created. - $this->addEntityIndex(); - $this->entityDefinitionUpdateManager->updateEntityType($entity_type); - - $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.'); - } - - /** - * Tests updating a fieldable entity type that doesn't exist in code anymore. - * - * @covers ::updateFieldableEntityType - */ - public function testUpdateFieldableEntityTypeWithoutInCodeDefinition(): void { - $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update'); - $field_storage_definitions = \Drupal::service('entity_field.manager')->getFieldStorageDefinitions('entity_test_update'); - - // Remove the entity type definition. This is the same thing as removing the - // code that defines it. - $this->deleteEntityType(); - - // Rename the base table, update the fieldable entity type and check that - // the table has been renamed. - $entity_type->set('base_table', 'entity_test_update_new'); - $this->entityDefinitionUpdateManager->updateFieldableEntityType($entity_type, $field_storage_definitions); - - $this->assertTrue($this->database->schema()->tableExists('entity_test_update_new'), 'The base table has been renamed.'); - $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'The old base table does not exist anymore.'); - } - - /** - * Tests uninstalling an entity type that doesn't exist in code anymore. - * - * @covers ::uninstallEntityType - */ - public function testUninstallEntityTypeWithoutInCodeDefinition(): void { - $entity_type = clone $this->entityTypeManager->getDefinition('entity_test_update'); - - // Remove the entity type definition. This is the same thing as removing the - // code that defines it. - $this->deleteEntityType(); - - // Now uninstall it and check that the tables have been removed. - $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update exists before uninstalling it.'); - $this->entityDefinitionUpdateManager->uninstallEntityType($entity_type); - $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update does not exist anymore.'); - } - - /** - * Tests uninstalling a revisionable entity type that doesn't exist in code. - * - * @covers ::uninstallEntityType - */ - public function testUninstallRevisionableEntityTypeWithoutInCodeDefinition(): void { - $this->updateEntityTypeToRevisionable(TRUE); - $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update'); - - // Remove the entity type definition. This is the same thing as removing the - // code that defines it. - $this->deleteEntityType(); - - // Now uninstall it and check that the tables have been removed. - $this->assertTrue($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update exists before uninstalling it.'); - $this->entityDefinitionUpdateManager->uninstallEntityType($entity_type); - $this->assertFalse($this->database->schema()->tableExists('entity_test_update'), 'Base table for entity_test_update does not exist anymore.'); - } - - /** - * Tests creating, updating, and deleting a base field if no entities exist. - */ - public function testBaseFieldCreateUpdateDeleteWithoutData(): void { - // Add a base field, ensure the update manager reports it, and the update - // creates its schema. - $this->addBaseField(); - $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); - $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); - $this->assertCount(1, $changes['entity_test_update']); - $this->assertEquals('The A new base field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0])); - $this->applyEntityUpdates(); - $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.'); - - // Add an index on the base field, ensure the update manager reports it, - // and the update creates it. - $this->addBaseFieldIndex(); - $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); - $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); - $this->assertCount(1, $changes['entity_test_update']); - $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0])); - $this->applyEntityUpdates(); - $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index created.'); - - // Remove the above index, ensure the update manager reports it, and the - // update deletes it. - $this->removeBaseFieldIndex(); - $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); - $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); - $this->assertCount(1, $changes['entity_test_update']); - $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0])); - $this->applyEntityUpdates(); - $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), 'Index deleted.'); - - // Update the type of the base field from 'string' to 'text', ensure the - // update manager reports it, and the update adjusts the schema - // accordingly. - $this->modifyBaseField(); - $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); - $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); - $this->assertCount(1, $changes['entity_test_update']); - $this->assertEquals('The A new base field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0])); - $this->applyEntityUpdates(); - $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), 'Original column deleted in shared table for new_base_field.'); - $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__value'), 'Value column created in shared table for new_base_field.'); - $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field__format'), 'Format column created in shared table for new_base_field.'); - - // Remove the base field, ensure the update manager reports it, and the - // update deletes the schema. - $this->removeBaseField(); - $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); - $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); - $this->assertCount(1, $changes['entity_test_update']); - $this->assertEquals('The A new base field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0])); - $this->applyEntityUpdates(); - $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_value'), 'Value column deleted from shared table for new_base_field.'); - $this->assertFalse($this->database->schema()->fieldExists('entity_test_update', 'new_base_field_format'), 'Format column deleted from shared table for new_base_field.'); - } - - /** - * Tests creating, updating, and deleting a base field with no label set. - * - * See testBaseFieldCreateUpdateDeleteWithoutData() for more details - */ - public function testBaseFieldWithoutLabelCreateUpdateDelete(): void { - // Add a base field, ensure the update manager reports it with the - // field id. - $this->addBaseField('string', 'entity_test_update', FALSE, FALSE); - $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); - $this->assertCount(1, $changes['entity_test_update']); - $this->assertEquals('The new_base_field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0])); - $this->applyEntityUpdates(); - - // Add an index on the base field, ensure the update manager reports it with - // the field id. - $this->addBaseFieldIndex(); - $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); - $this->assertCount(1, $changes['entity_test_update']); - $this->assertEquals('The new_base_field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0])); - $this->applyEntityUpdates(); - - // Remove the base field, ensure the update manager reports it with the - // field id. - $this->removeBaseField(); - $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); - $this->assertCount(1, $changes['entity_test_update']); - $this->assertEquals('The new_base_field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0])); - } - - /** - * Tests creating, updating, and deleting a bundle field if no entities exist. - */ - public function testBundleFieldCreateUpdateDeleteWithoutData(): void { - // Add a bundle field, ensure the update manager reports it, and the update - // creates its schema. - $this->addBundleField(); - $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); - $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); - $this->assertCount(1, $changes['entity_test_update']); - $this->assertEquals('The A new bundle field field needs to be installed.', strip_tags((string) $changes['entity_test_update'][0])); - $this->applyEntityUpdates(); - $this->assertTrue($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.'); - - // Update the type of the base field from 'string' to 'text', ensure the - // update manager reports it, and the update adjusts the schema - // accordingly. - $this->modifyBundleField(); - $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); - $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); - $this->assertCount(1, $changes['entity_test_update']); - $this->assertEquals('The A new bundle field field needs to be updated.', strip_tags((string) $changes['entity_test_update'][0])); - $this->applyEntityUpdates(); - $this->assertTrue($this->database->schema()->fieldExists('entity_test_update__new_bundle_field', 'new_bundle_field_format'), 'Format column created in dedicated table for new_base_field.'); - - // Remove the bundle field, ensure the update manager reports it, and the - // update deletes the schema. - $this->removeBundleField(); - $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); - $changes = $this->entityDefinitionUpdateManager->getChangeSummary(); - $this->assertCount(1, $changes['entity_test_update']); - $this->assertEquals('The A new bundle field field needs to be uninstalled.', strip_tags((string) $changes['entity_test_update'][0])); - $this->applyEntityUpdates(); - $this->assertFalse($this->database->schema()->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.'); - } - - /** - * Tests creating and deleting a base field if entities exist. - * - * This tests deletion when there are existing entities, but non-existent data - * for the field being deleted. - * - * @see testBaseFieldDeleteWithExistingData() - */ - public function testBaseFieldCreateDeleteWithExistingEntities(): void { - // Save an entity. - $name = $this->randomString(); - $storage = $this->entityTypeManager->getStorage('entity_test_update'); - $entity = $storage->create(['name' => $name]); - $entity->save(); - - // Add a base field and run the update. Ensure the base field's column is - // created and the prior saved entity data is still there. - $this->addBaseField(); - $this->applyEntityUpdates(); - $schema_handler = $this->database->schema(); - $this->assertTrue($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column created in shared table for new_base_field.'); - $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id()); - $this->assertSame($name, $entity->name->value, 'Entity data preserved during field creation.'); - - // Remove the base field and run the update. Ensure the base field's column - // is deleted and the prior saved entity data is still there. - $this->removeBaseField(); - $this->applyEntityUpdates(); - $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field'), 'Column deleted from shared table for new_base_field.'); - $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id()); - $this->assertSame($name, $entity->name->value, 'Entity data preserved during field deletion.'); - - // Add a base field with a required property and run the update. Ensure - // 'not null' is not applied and thus no exception is thrown. - $this->addBaseField('shape_required'); - $this->applyEntityUpdates(); - $assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color'); - $this->assertTrue($assert, 'Columns created in shared table for new_base_field.'); - - // Recreate the field after emptying the base table and check that its - // columns are not 'not null'. - // @todo Revisit this test when allowing for required storage field - // definitions. See https://www.drupal.org/node/2390495. - $entity->delete(); - $this->removeBaseField(); - $this->applyEntityUpdates(); - $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field__shape'), 'Shape column should be removed from the shared table for new_base_field.'); - $this->assertFalse($schema_handler->fieldExists('entity_test_update', 'new_base_field__color'), 'Color column should be removed from the shared table for new_base_field.'); - $this->addBaseField('shape_required'); - $this->applyEntityUpdates(); - $assert = $schema_handler->fieldExists('entity_test_update', 'new_base_field__shape') && $schema_handler->fieldExists('entity_test_update', 'new_base_field__color'); - $this->assertTrue($assert, 'Columns created again in shared table for new_base_field.'); - $entity = $storage->create(['name' => $name]); - $entity->save(); - } - - /** - * Tests creating and deleting a bundle field if entities exist. - * - * This tests deletion when there are existing entities, but non-existent data - * for the field being deleted. - * - * @see testBundleFieldDeleteWithExistingData() - */ - public function testBundleFieldCreateDeleteWithExistingEntities(): void { - // Save an entity. - $name = $this->randomString(); - $storage = $this->entityTypeManager->getStorage('entity_test_update'); - $entity = $storage->create(['name' => $name]); - $entity->save(); - - // Add a bundle field and run the update. Ensure the bundle field's table - // is created and the prior saved entity data is still there. - $this->addBundleField(); - $this->applyEntityUpdates(); - $schema_handler = $this->database->schema(); - $this->assertTrue($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table created for new_bundle_field.'); - $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id()); - $this->assertSame($name, $entity->name->value, 'Entity data preserved during field creation.'); - - // Remove the base field and run the update. Ensure the bundle field's - // table is deleted and the prior saved entity data is still there. - $this->removeBundleField(); - $this->applyEntityUpdates(); - $this->assertFalse($schema_handler->tableExists('entity_test_update__new_bundle_field'), 'Dedicated table deleted for new_bundle_field.'); - $entity = $this->entityTypeManager->getStorage('entity_test_update')->load($entity->id()); - $this->assertSame($name, $entity->name->value, 'Entity data preserved during field deletion.'); - - // Test that required columns are created as 'not null'. - $this->addBundleField('shape_required'); - $this->applyEntityUpdates(); - $message = 'The new_bundle_field_shape column is not nullable.'; - $values = [ - 'bundle' => $entity->bundle(), - 'deleted' => 0, - 'entity_id' => $entity->id(), - 'revision_id' => $entity->id(), - 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, - 'delta' => 0, - 'new_bundle_field_color' => $this->randomString(), - ]; - try { - // Try to insert a record without providing a value for the 'not null' - // column. This should fail. - $this->database->insert('entity_test_update__new_bundle_field') - ->fields($values) - ->execute(); - $this->fail($message); - } - catch (IntegrityConstraintViolationException) { - // Now provide a value for the 'not null' column. This is expected to - // succeed. - $values['new_bundle_field_shape'] = $this->randomString(); - $this->database->insert('entity_test_update__new_bundle_field') - ->fields($values) - ->execute(); - } - } - - /** - * Tests deleting a bundle field when it has existing data. - */ - public function testBundleFieldDeleteWithExistingData(): void { - /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */ - $storage = $this->entityTypeManager->getStorage('entity_test_update'); - $schema_handler = $this->database->schema(); - - // Add the bundle field and run the update. - $this->addBundleField(); - $this->applyEntityUpdates(); - - /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ - $table_mapping = $storage->getTableMapping(); - $storage_definition = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledFieldStorageDefinitions('entity_test_update')['new_bundle_field']; - - // Check that the bundle field has a dedicated table. - $dedicated_table_name = $table_mapping->getDedicatedDataTableName($storage_definition); - $this->assertTrue($schema_handler->tableExists($dedicated_table_name), 'The bundle field uses a dedicated table.'); - - // Save an entity with the bundle field populated. - EntityTestHelper::createBundle('custom'); - $entity = $storage->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo']); - $entity->save(); - - // Remove the bundle field and apply updates. - $this->removeBundleField(); - $this->applyEntityUpdates(); - - // Check that the table of the bundle field has been renamed to use a - // 'deleted' table name. - $this->assertFalse($schema_handler->tableExists($dedicated_table_name), 'The dedicated table of the bundle field no longer exists.'); - - $dedicated_deleted_table_name = $table_mapping->getDedicatedDataTableName($storage_definition, TRUE); - $this->assertTrue($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle fields has been renamed to use the "deleted" name.'); - - // Check that the deleted field's data is preserved in the dedicated - // 'deleted' table. - $result = $this->database->select($dedicated_deleted_table_name, 't') - ->fields('t') - ->execute() - ->fetchAll(); - $this->assertCount(1, $result); - - $expected = [ - 'bundle' => $entity->bundle(), - 'deleted' => '1', - 'entity_id' => $entity->id(), - 'revision_id' => $entity->id(), - 'langcode' => $entity->language()->getId(), - 'delta' => '0', - 'new_bundle_field_value' => $entity->new_bundle_field->value, - ]; - // Use assertEquals and not assertSame here to prevent that a different - // sequence of the columns in the table will affect the check. - $this->assertEquals($expected, (array) $result[0]); - - // Check that the field definition is marked for purging. - $deleted_field_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldDefinitions(); - $this->assertArrayHasKey($storage_definition->getUniqueIdentifier(), $deleted_field_definitions, 'The bundle field is marked for purging.'); - - // Check that the field storage definition is marked for purging. - $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions(); - $this->assertArrayHasKey($storage_definition->getUniqueStorageIdentifier(), $deleted_storage_definitions, 'The bundle field storage is marked for purging.'); - - // Purge field data, and check that the storage definition has been - // completely removed once the data is purged. - field_purge_batch(10); - $deleted_field_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldDefinitions(); - $this->assertEmpty($deleted_field_definitions, 'The bundle field has been deleted.'); - $deleted_storage_definitions = \Drupal::service('entity_field.deleted_fields_repository')->getFieldStorageDefinitions(); - $this->assertEmpty($deleted_storage_definitions, 'The bundle field storage has been deleted.'); - $this->assertFalse($schema_handler->tableExists($dedicated_deleted_table_name), 'The dedicated table of the bundle field has been removed.'); - } - - /** - * Tests updating a base field when it has existing data. - */ - public function testBaseFieldUpdateWithExistingData(): void { - // Add the base field and run the update. - $this->addBaseField(); - $this->applyEntityUpdates(); - - // Save an entity with the base field populated. - $this->entityTypeManager->getStorage('entity_test_update')->create(['new_base_field' => 'foo'])->save(); - - // Change the field's field type and apply updates. It's expected to - // throw an exception. - $this->modifyBaseField(); - try { - $this->applyEntityUpdates(); - $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.'); - } - catch (FieldStorageDefinitionUpdateForbiddenException) { - // Expected exception; just continue testing. - } - } - - /** - * Tests updating a bundle field when it has existing data. - */ - public function testBundleFieldUpdateWithExistingData(): void { - // Add the bundle field and run the update. - $this->addBundleField(); - $this->applyEntityUpdates(); - - // Save an entity with the bundle field populated. - EntityTestHelper::createBundle('custom'); - $this->entityTypeManager->getStorage('entity_test_update')->create(['type' => 'test_bundle', 'new_bundle_field' => 'foo'])->save(); - - // Change the field's field type and apply updates. It's expected to - // throw an exception. - $this->modifyBundleField(); - try { - $this->applyEntityUpdates(); - $this->fail('FieldStorageDefinitionUpdateForbiddenException thrown when trying to update a field schema that has data.'); - } - catch (FieldStorageDefinitionUpdateForbiddenException) { - // Expected exception; just continue testing. - } - } - - /** - * Tests updating a bundle field when the entity type schema has changed. - */ - public function testBundleFieldUpdateWithEntityTypeSchemaUpdate(): void { - // Add the bundle field and run the update. - $this->addBundleField(); - $this->applyEntityUpdates(); - - // Update the entity type schema to revisionable but don't run the updates - // yet. - $this->updateEntityTypeToRevisionable(); - - // Perform a no-op update on the bundle field, which should work because - // both the storage and the storage schema are using the last installed - // entity type definition. - $entity_definition_update_manager = \Drupal::entityDefinitionUpdateManager(); - $entity_definition_update_manager->updateFieldStorageDefinition($entity_definition_update_manager->getFieldStorageDefinition('new_bundle_field', 'entity_test_update')); - } - - /** - * Tests creating and deleting a multi-field index when there are no existing entities. - */ - public function testEntityIndexCreateDeleteWithoutData(): void { - // Add an entity index and ensure the update manager reports that as an - // update to the entity type. - $this->addEntityIndex(); - $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); - $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel(); - $expected = [ - 'entity_test_update' => [ - "The $entity_type entity type needs to be updated.", - ], - ]; - $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.'); - - // Run the update and ensure the new index is created. - $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update'); - $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update'); - \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original); - $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.'); - - // Remove the index and ensure the update manager reports that as an - // update to the entity type. - $this->removeEntityIndex(); - $this->assertTrue($this->entityDefinitionUpdateManager->needsUpdates(), 'EntityDefinitionUpdateManager reports that updates are needed.'); - $entity_type = $this->entityTypeManager->getDefinition('entity_test_update')->getLabel(); - $expected = [ - 'entity_test_update' => [ - "The $entity_type entity type needs to be updated.", - ], - ]; - $this->assertEquals($expected, $this->entityDefinitionUpdateManager->getChangeSummary(), 'EntityDefinitionUpdateManager reports the expected change summary.'); - - // Run the update and ensure the index is deleted. - $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update'); - $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update'); - \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original); - $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.'); - - // Test that composite indexes are handled correctly when dropping and - // re-creating one of their columns. - $this->addEntityIndex(); - $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update'); - $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update'); - \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original); - - $storage_definition = $this->entityDefinitionUpdateManager->getFieldStorageDefinition('name', 'entity_test_update'); - $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition); - $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created.'); - $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition); - $this->assertFalse($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index deleted.'); - $this->entityDefinitionUpdateManager->installFieldStorageDefinition('name', 'entity_test_update', 'entity_test', $storage_definition); - $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index created again.'); - } - - /** - * Tests creating a multi-field index when there are existing entities. - */ - public function testEntityIndexCreateWithData(): void { - // Save an entity. - $name = $this->randomString(); - $entity = $this->entityTypeManager->getStorage('entity_test_update')->create(['name' => $name]); - $entity->save(); - - // Add an entity index, run the update. Ensure that the index is created - // despite having data. - $this->addEntityIndex(); - $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update'); - $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update'); - \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original); - $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__new_index'), 'Index added.'); - } - - /** - * Tests entity type and field storage definition events. - */ - public function testDefinitionEvents(): void { - /** @var \Drupal\entity_test\EntityTestDefinitionSubscriber $event_subscriber */ - $event_subscriber = $this->container->get('entity_test.definition.subscriber'); - $event_subscriber->enableEventTracking(); - $event_subscriber->enableLiveDefinitionUpdates(); - - // Test field storage definition events. - $storage_definition = FieldStorageDefinition::create('string') - ->setName('field_storage_test') - ->setLabel(new TranslatableMarkup('Field storage test')) - ->setTargetEntityTypeId('entity_test_rev'); - - $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create was not dispatched yet.'); - \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionCreate($storage_definition); - $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::CREATE), 'Entity type create event successfully dispatched.'); - $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::CREATE), 'Last installed field storage definition was created before the event was fired.'); - - // Check that the newly added field can be retrieved from the live field - // storage definitions. - $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev'); - $this->assertArrayHasKey('field_storage_test', $field_storage_definitions); - - $updated_storage_definition = clone $storage_definition; - $updated_storage_definition->setLabel(new TranslatableMarkup('Updated field storage test')); - $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update was not dispatched yet.'); - \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionUpdate($updated_storage_definition, $storage_definition); - $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::UPDATE), 'Entity type update event successfully dispatched.'); - $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::UPDATE), 'Last installed field storage definition was updated before the event was fired.'); - - // Check that the updated field can be retrieved from the live field storage - // definitions. - $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev'); - $this->assertEquals(new TranslatableMarkup('Updated field storage test'), $field_storage_definitions['field_storage_test']->getLabel()); - - $this->assertFalse($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete was not dispatched yet.'); - \Drupal::service('field_storage_definition.listener')->onFieldStorageDefinitionDelete($storage_definition); - $this->assertTrue($event_subscriber->hasEventFired(FieldStorageDefinitionEvents::DELETE), 'Entity type delete event successfully dispatched.'); - $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(FieldStorageDefinitionEvents::DELETE), 'Last installed field storage definition was deleted before the event was fired.'); - - // Check that the deleted field can no longer be retrieved from the live - // field storage definitions. - $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions('entity_test_rev'); - $this->assertArrayNotHasKey('field_storage_test', $field_storage_definitions); - - // Test entity type events. - $entity_type = $this->entityTypeManager->getDefinition('entity_test_rev'); - - $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create was not dispatched yet.'); - \Drupal::service('entity_type.listener')->onEntityTypeCreate($entity_type); - $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::CREATE), 'Entity type create event successfully dispatched.'); - $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::CREATE), 'Last installed entity type definition was created before the event was fired.'); - - $updated_entity_type = clone $entity_type; - $updated_entity_type->set('label', new TranslatableMarkup('Updated entity test rev')); - $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update was not dispatched yet.'); - \Drupal::service('entity_type.listener')->onEntityTypeUpdate($updated_entity_type, $entity_type); - $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::UPDATE), 'Entity type update event successfully dispatched.'); - $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::UPDATE), 'Last installed entity type definition was updated before the event was fired.'); - - // Check that the updated definition can be retrieved from the live entity - // type definitions. - $entity_type = $this->entityTypeManager->getDefinition('entity_test_rev'); - $this->assertEquals(new TranslatableMarkup('Updated entity test rev'), $entity_type->getLabel()); - - $this->assertFalse($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete was not dispatched yet.'); - \Drupal::service('entity_type.listener')->onEntityTypeDelete($entity_type); - $this->assertTrue($event_subscriber->hasEventFired(EntityTypeEvents::DELETE), 'Entity type delete event successfully dispatched.'); - $this->assertTrue($event_subscriber->hasDefinitionBeenUpdated(EntityTypeEvents::DELETE), 'Last installed entity type definition was deleted before the event was fired.'); - - // Check that the deleted entity type can no longer be retrieved from the - // live entity type definitions. - $this->assertNull($this->entityTypeManager->getDefinition('entity_test_rev', FALSE)); - } - - /** - * Tests applying single updates. - */ - public function testSingleActionCalls(): void { - $db_schema = $this->database->schema(); - - // Ensure that a non-existing entity type cannot be installed. - $message = 'A non-existing entity type cannot be installed'; - try { - $this->entityDefinitionUpdateManager->installEntityType(new ContentEntityType(['id' => 'foo'])); - $this->fail($message); - } - catch (PluginNotFoundException) { - // Expected exception; just continue testing. - } - - // Ensure that a field cannot be installed on non-existing entity type. - $message = 'A field cannot be installed on a non-existing entity type'; - try { - $storage_definition = BaseFieldDefinition::create('string') - ->setLabel('A new revisionable base field') - ->setRevisionable(TRUE); - $this->entityDefinitionUpdateManager->installFieldStorageDefinition('bar', 'foo', 'entity_test', $storage_definition); - $this->fail($message); - } - catch (PluginNotFoundException) { - // Expected exception; just continue testing. - } - - // Ensure that installing an existing entity type is a no-op. - $entity_type = $this->entityDefinitionUpdateManager->getEntityType('entity_test_update'); - $this->entityDefinitionUpdateManager->installEntityType($entity_type); - $this->assertTrue($db_schema->tableExists('entity_test_update'), 'Installing an existing entity type is a no-op'); - - // Create a new base field. - $this->addRevisionableBaseField(); - $storage_definition = BaseFieldDefinition::create('string') - ->setLabel('A new revisionable base field') - ->setRevisionable(TRUE); - $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update."); - $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); - $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table."); - - // Ensure that installing an existing field is a no-op. - $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); - $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), 'Installing an existing field is a no-op'); - - // Update an existing field schema. - $this->modifyBaseField(); - $storage_definition = BaseFieldDefinition::create('text') - ->setName('new_base_field') - ->setTargetEntityTypeId('entity_test_update') - ->setLabel('A new revisionable base field') - ->setRevisionable(TRUE); - $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($storage_definition); - $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "Previous schema for 'new_base_field' no longer exists."); - $this->assertTrue( - $db_schema->fieldExists('entity_test_update', 'new_base_field__value') && $db_schema->fieldExists('entity_test_update', 'new_base_field__format'), - "New schema for 'new_base_field' has been created." - ); - - // Drop an existing field schema. - $this->entityDefinitionUpdateManager->uninstallFieldStorageDefinition($storage_definition); - $this->assertFalse( - $db_schema->fieldExists('entity_test_update', 'new_base_field__value') || $db_schema->fieldExists('entity_test_update', 'new_base_field__format'), - "The schema for 'new_base_field' has been dropped." - ); - - // Make the entity type revisionable. - $this->updateEntityTypeToRevisionable(); - $this->assertFalse($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' does not exist before applying the update."); - - $this->updateEntityTypeToRevisionable(TRUE); - $this->assertTrue($db_schema->tableExists('entity_test_update_revision'), "The 'entity_test_update_revision' table has been created."); - } - - /** - * Ensures that a new field and index on a shared table are created. - * - * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::createSharedTableSchema - */ - public function testCreateFieldAndIndexOnSharedTable(): void { - $this->addBaseField(); - $this->addBaseFieldIndex(); - $this->applyEntityUpdates(); - $this->assertTrue($this->database->schema()->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table."); - $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update_field__new_base_field'), "New index 'entity_test_update_field__new_base_field' has been created on the 'entity_test_update' table."); - // Check index size in for MySQL. - if (Database::getConnection()->driver() == 'mysql') { - $result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update_field__new_base_field\' and column_name = \'new_base_field\'')->fetchObject(); - $this->assertEquals(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.'); - } - } - - /** - * Ensures that a new entity level index is created when data exists. - * - * @see Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema::onEntityTypeUpdate - */ - public function testCreateIndexUsingEntityStorageSchemaWithData(): void { - // Save an entity. - $name = $this->randomString(); - $storage = $this->entityTypeManager->getStorage('entity_test_update'); - $entity = $storage->create(['name' => $name]); - $entity->save(); - - // Create an index. - $indexes = [ - 'entity_test_update__type_index' => ['type'], - ]; - $this->state->set('entity_test_update.additional_entity_indexes', $indexes); - $entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_update'); - $original = \Drupal::service('entity.last_installed_schema.repository')->getLastInstalledDefinition('entity_test_update'); - \Drupal::service('entity_type.listener')->onEntityTypeUpdate($entity_type, $original); - - $this->assertTrue($this->database->schema()->indexExists('entity_test_update', 'entity_test_update__type_index'), "New index 'entity_test_update__type_index' has been created on the 'entity_test_update' table."); - // Check index size in for MySQL. - if (Database::getConnection()->driver() == 'mysql') { - $result = Database::getConnection()->query('SHOW INDEX FROM {entity_test_update} WHERE key_name = \'entity_test_update__type_index\' and column_name = \'type\'')->fetchObject(); - $this->assertEquals(191, $result->Sub_part, 'The index length has been restricted to 191 characters for UTF8MB4 compatibility.'); - } - } - - /** - * Tests updating a base field when it has existing data. - */ - public function testBaseFieldEntityKeyUpdateWithExistingData(): void { - // Add the base field and run the update. - $this->addBaseField(); - $this->applyEntityUpdates(); - - // Save an entity with the base field populated. - $this->entityTypeManager->getStorage('entity_test_update')->create(['new_base_field' => $this->randomString()])->save(); - - // Save an entity with the base field not populated. - /** @var \Drupal\entity_test\Entity\EntityTestUpdate $entity */ - $entity = $this->entityTypeManager->getStorage('entity_test_update')->create(); - $entity->save(); - - // Promote the base field to an entity key. This will trigger the addition - // of a NOT NULL constraint. - $this->makeBaseFieldEntityKey(); - - // Field storage CRUD operations use the last installed entity type - // definition so we need to update it before doing any other field storage - // updates. - $this->entityDefinitionUpdateManager->updateEntityType($this->state->get('entity_test_update.entity_type')); - - // Try to apply the update and verify they fail since we have a NULL value. - $message = 'An error occurs when trying to enabling NOT NULL constraints with NULL data.'; - try { - $this->applyEntityUpdates(); - $this->fail($message); - } - catch (EntityStorageException) { - // Expected exception; just continue testing. - } - - // Check that the update is correctly applied when no NULL data is left. - $entity->set('new_base_field', $this->randomString()); - $entity->save(); - $this->applyEntityUpdates(); - - // Check that the update actually applied a NOT NULL constraint. - $entity->set('new_base_field', NULL); - $message = 'The NOT NULL constraint was correctly applied.'; - try { - $entity->save(); - $this->fail($message); - } - catch (EntityStorageException) { - // Expected exception; just continue testing. - } - } - - /** - * Check that field schema is correctly handled with long-named fields. - */ - public function testLongNameFieldIndexes(): void { - $this->addLongNameBaseField(); - $entity_type_id = 'entity_test_update'; - $entity_type = $this->entityTypeManager->getDefinition($entity_type_id); - $definitions = EntityTestUpdate::baseFieldDefinitions($entity_type); - $name = 'new_long_named_entity_reference_base_field'; - $this->entityDefinitionUpdateManager->installFieldStorageDefinition($name, $entity_type_id, 'entity_test', $definitions[$name]); - $this->assertFalse($this->entityDefinitionUpdateManager->needsUpdates(), 'Entity and field schema data are correctly detected.'); - } - - /** - * Tests adding a base field with initial values. - */ - public function testInitialValue(): void { - $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update'); - $db_schema = $this->database->schema(); - - // Create two entities before adding the base field. - $storage->create()->save(); - $storage->create()->save(); - - // Add a base field with an initial value. - $this->addBaseField(); - $storage_definition = BaseFieldDefinition::create('string') - ->setLabel('A new base field') - ->setInitialValue('test value'); - - $this->assertFalse($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' does not exist before applying the update."); - $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); - $this->assertTrue($db_schema->fieldExists('entity_test_update', 'new_base_field'), "New field 'new_base_field' has been created on the 'entity_test_update' table."); - - // Check that the initial values have been applied. - $storage = \Drupal::entityTypeManager()->getStorage('entity_test_update'); - $entities = $storage->loadMultiple(); - $this->assertEquals('test value', $entities[1]->get('new_base_field')->value); - $this->assertEquals('test value', $entities[2]->get('new_base_field')->value); - } - - /** - * Tests the error handling when using initial values from another field. - */ - public function testInitialValueFromFieldErrorHandling(): void { - // Check that setting invalid values for 'initial value from field' doesn't - // work. - try { - $this->addBaseField(); - $storage_definition = BaseFieldDefinition::create('string') - ->setLabel('A new base field') - ->setInitialValueFromField('field_that_does_not_exist'); - $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); - $this->fail('Using a non-existent field as initial value does not work.'); - } - catch (FieldException $e) { - $this->assertEquals('Illegal initial value definition on new_base_field: The field field_that_does_not_exist does not exist.', $e->getMessage()); - } - - try { - $this->addBaseField(); - $storage_definition = BaseFieldDefinition::create('integer') - ->setLabel('A new base field') - ->setInitialValueFromField('name'); - $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $storage_definition); - $this->fail('Using a field of a different type as initial value does not work.'); - } - catch (FieldException $e) { - $this->assertEquals('Illegal initial value definition on new_base_field: The field types do not match.', $e->getMessage()); - } - - try { - // Add a base field that will not be stored in the shared tables. - $initial_field = BaseFieldDefinition::create('string') - ->setName('initial_field') - ->setLabel('An initial field') - ->setCardinality(2); - $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field]); - $this->entityDefinitionUpdateManager->installFieldStorageDefinition('initial_field', 'entity_test_update', 'entity_test', $initial_field); - - // Now add the base field which will try to use the previously added field - // as the source of its initial values. - $new_base_field = BaseFieldDefinition::create('string') - ->setName('new_base_field') - ->setLabel('A new base field') - ->setInitialValueFromField('initial_field'); - $this->state->set('entity_test_update.additional_base_field_definitions', ['initial_field' => $initial_field, 'new_base_field' => $new_base_field]); - $this->entityDefinitionUpdateManager->installFieldStorageDefinition('new_base_field', 'entity_test_update', 'entity_test', $new_base_field); - $this->fail('Using a field that is not stored in the shared tables as initial value does not work.'); - } - catch (FieldException $e) { - $this->assertEquals('Illegal initial value definition on new_base_field: Both fields have to be stored in the shared entity tables.', $e->getMessage()); - } - } - - /** * @covers ::getEntityTypes */ public function testGetEntityTypes(): void { diff --git a/core/tests/Drupal/KernelTests/Core/Extension/LegacyRequirementSeverityTest.php b/core/tests/Drupal/KernelTests/Core/Extension/LegacyRequirementSeverityTest.php new file mode 100644 index 000000000000..020ef21c2273 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Extension/LegacyRequirementSeverityTest.php @@ -0,0 +1,86 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Extension; + +use Drupal\KernelTests\KernelTestBase; + +include_once \DRUPAL_ROOT . '/core/includes/install.inc'; + +/** + * Tests the legacy requirements severity deprecations. + * + * @coversDefaultClass \Drupal\Core\Extension\Requirement\RequirementSeverity + * @group extension + * @group legacy + */ +class LegacyRequirementSeverityTest extends KernelTestBase { + + /** + * @covers \drupal_requirements_severity + * @dataProvider requirementProvider + */ + public function testGetMaxSeverity(array $requirements, int $expectedSeverity): void { + $this->expectDeprecation( + 'drupal_requirements_severity() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use Drupal\Core\Extension\Requirement\RequirementSeverity::maxSeverityFromRequirements() instead. See https://www.drupal.org/node/3410939' + ); + $this->expectDeprecation( + 'Calling Drupal\Core\Extension\Requirement\RequirementSeverity::maxSeverityFromRequirements() with an array of $requirements with \'severity\' with values not of type Drupal\Core\Extension\Requirement\RequirementSeverity enums is deprecated in drupal:11.2.0 and is required in drupal:12.0.0. See https://www.drupal.org/node/3410939' + ); + $severity = drupal_requirements_severity($requirements); + $this->assertEquals($expectedSeverity, $severity); + } + + /** + * Data provider for requirement helper test. + * + * @return array + * Test data. + */ + public static function requirementProvider(): array { + $info = [ + 'title' => 'Foo', + 'severity' => \REQUIREMENT_INFO, + ]; + $warning = [ + 'title' => 'Baz', + 'severity' => \REQUIREMENT_WARNING, + ]; + $error = [ + 'title' => 'Wiz', + 'severity' => \REQUIREMENT_ERROR, + ]; + $ok = [ + 'title' => 'Bar', + 'severity' => \REQUIREMENT_OK, + ]; + + return [ + 'error is most severe' => [ + [ + $info, + $error, + $ok, + ], + \REQUIREMENT_ERROR, + ], + 'ok is most severe' => [ + [ + $info, + $ok, + ], + \REQUIREMENT_OK, + ], + 'warning is most severe' => [ + [ + $warning, + $info, + $ok, + ], + \REQUIREMENT_WARNING, + ], + ]; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php index 8ee28a968fd5..c90cb5e0aad7 100644 --- a/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php +++ b/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Drupal\KernelTests\Core\Extension; use Drupal\Core\Extension\Exception\UnknownExtensionException; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\KernelTests\KernelTestBase; /** @@ -36,4 +37,78 @@ class ModuleHandlerTest extends KernelTestBase { $this->assertNotNull(\Drupal::service('module_handler')->getName('module_test')); } + /** + * Tests that resetImplementations() clears the hook memory cache. + * + * @covers ::resetImplementations + */ + public function testResetImplementationsClearsHooks(): void { + $oldModuleHandler = \Drupal::moduleHandler(); + $this->assertHasResetHookImplementations(FALSE, $oldModuleHandler); + + // Installing a module does not trigger ->resetImplementations(). + /** @var \Drupal\Core\Extension\ModuleInstallerInterface $moduleInstaller */ + $moduleInstaller = \Drupal::service('module_installer'); + $moduleInstaller->install(['module_test']); + $this->assertHasResetHookImplementations(FALSE, $oldModuleHandler); + // Only the new ModuleHandler instance has the updated implementations. + $moduleHandler = \Drupal::moduleHandler(); + $this->assertHasResetHookImplementations(TRUE, $moduleHandler); + $backupModuleList = $moduleHandler->getModuleList(); + $moduleListWithout = array_diff_key($backupModuleList, ['module_test' => TRUE]); + $this->assertArrayHasKey('module_test', $backupModuleList); + + // Silently setting the property does not clear the hooks cache. + $moduleListProperty = (new \ReflectionProperty($moduleHandler, 'moduleList')); + $this->assertSame($backupModuleList, $moduleListProperty->getValue($moduleHandler)); + $moduleListProperty->setValue($moduleHandler, $moduleListWithout); + $this->assertHasResetHookImplementations(TRUE, $moduleHandler); + + // Directly calling ->resetImplementations() clears the hook caches. + $moduleHandler->resetImplementations(); + $this->assertHasResetHookImplementations(FALSE, $moduleHandler); + $moduleListProperty->setValue($moduleHandler, $backupModuleList); + $this->assertHasResetHookImplementations(FALSE, $moduleHandler); + $moduleHandler->resetImplementations(); + $this->assertHasResetHookImplementations(TRUE, $moduleHandler); + + // Calling ->setModuleList() triggers ->resetImplementations(). + $moduleHandler->setModuleList(['system']); + $this->assertHasResetHookImplementations(FALSE, $moduleHandler); + $moduleHandler->setModuleList($backupModuleList); + $this->assertHasResetHookImplementations(TRUE, $moduleHandler); + + // Uninstalling a module triggers ->resetImplementations(). + /** @var \Drupal\Core\Extension\ModuleInstallerInterface $moduleInstaller */ + $moduleInstaller = \Drupal::service('module_installer'); + $moduleInstaller->uninstall(['module_test']); + $this->assertSame($moduleListWithout, $moduleHandler->getModuleList()); + $this->assertHasResetHookImplementations(FALSE, $moduleHandler); + } + + /** + * Asserts whether certain hook implementations exist. + * + * This is used to verify that all internal hook cache properties have been + * reset and updated. + * + * @param bool $exists + * TRUE if the implementations are expected to exist, FALSE if not. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler + * The module handler. + * + * @see \module_test_test_reset_implementations_hook() + * @see \module_test_test_reset_implementations_alter() + */ + protected function assertHasResetHookImplementations(bool $exists, ModuleHandlerInterface $moduleHandler): void { + $this->assertSame($exists, $moduleHandler->hasImplementations('test_reset_implementations_hook')); + $this->assertSame($exists, $moduleHandler->hasImplementations('test_reset_implementations_alter')); + $expected_list = $exists ? ['module_test_test_reset_implementations_hook'] : []; + $this->assertSame($expected_list, $moduleHandler->invokeAll('test_reset_implementations_hook')); + $expected_alter_list = $exists ? ['module_test_test_reset_implementations_alter'] : []; + $alter_list = []; + $moduleHandler->alter('test_reset_implementations', $alter_list); + $this->assertSame($expected_alter_list, $alter_list); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/File/FileSystemRequirementsTest.php b/core/tests/Drupal/KernelTests/Core/File/FileSystemRequirementsTest.php index 0cb098bfccab..ca1fab258f3c 100644 --- a/core/tests/Drupal/KernelTests/Core/File/FileSystemRequirementsTest.php +++ b/core/tests/Drupal/KernelTests/Core/File/FileSystemRequirementsTest.php @@ -45,8 +45,14 @@ class FileSystemRequirementsTest extends KernelTestBase { * An array of system requirements. */ protected function checkSystemRequirements() { + // This loadInclude() is to ensure that the install API is available. + // Since we're loading an include of type 'install', this will also + // include core/includes/install.inc for us, which is where + // drupal_verify_install_file() is currently defined. + // @todo Remove this once the function lives in a better place. + // @see https://www.drupal.org/project/drupal/issues/3526388 $this->container->get('module_handler')->loadInclude('system', 'install'); - return system_requirements('runtime'); + return \Drupal::moduleHandler()->invoke('system', 'runtime_requirements'); } } diff --git a/core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php b/core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php index dfe537e4d1fa..7d1e641f485a 100644 --- a/core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php +++ b/core/tests/Drupal/KernelTests/Core/File/HtaccessTest.php @@ -92,6 +92,15 @@ class HtaccessTest extends KernelTestBase { } /** + * @covers ::write + */ + public function testHtaccessSaveDisabled(): void { + $this->setSetting('auto_create_htaccess', FALSE); + $this->assertTrue($this->htaccessWriter->write($this->public, FALSE)); + $this->assertFileDoesNotExist($this->public . '/.htaccess'); + } + + /** * Asserts expected file permissions for a given file. * * @param string $uri diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php index ffc5a85fdb0b..8fc0c2013d29 100644 --- a/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php +++ b/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php @@ -17,7 +17,9 @@ use Drupal\FunctionalTests\Core\Recipe\RecipeTestTrait; use Drupal\KernelTests\KernelTestBase; use Drupal\node\Entity\NodeType; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\StyleInterface; +use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Validator\Exception\ValidationFailedException; /** @@ -289,4 +291,82 @@ YAML RecipeRunner::processRecipe($recipe); } + /** + * Tests that the askHidden prompt forwards arguments correctly. + */ + public function testAskHiddenPromptArgumentsForwarded(): void { + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(OutputInterface::class); + $io = new SymfonyStyle($input, $output); + + $recipe = $this->createRecipe(<<<YAML +name: 'Prompt askHidden Test' +input: + foo: + data_type: string + description: Foo + prompt: + method: askHidden + default: + source: value + value: bar +YAML + ); + $collector = new ConsoleInputCollector($input, $io); + // askHidden prompt should have an ArgumentCountError rather than a general + // error. + $this->expectException(\ArgumentCountError::class); + $recipe->input->collectAll($collector); + } + + /** + * Tests getting default input values from environment variables. + */ + public function testDefaultInputFromEnvironmentVariables(): void { + $this->config('system.site') + ->set('name', 'Hello Thar') + ->set('slogan', 'Very important') + ->save(); + + $recipe = $this->createRecipe(<<<YAML +name: 'Input from environment variables' +input: + name: + data_type: string + description: The name of the site. + default: + source: env + env: SITE_NAME + slogan: + data_type: string + description: The site slogan. + default: + source: env + env: SITE_SLOGAN +config: + actions: + system.site: + simpleConfigUpdate: + name: \${name} + slogan: \${slogan} +YAML + ); + putenv('SITE_NAME=Input Test'); + + // Mock a collector that only returns the default value. + $collector = $this->createMock(InputCollectorInterface::class); + $collector->expects($this->any()) + ->method('collectValue') + ->withAnyParameters() + ->willReturnArgument(2); + $recipe->input->collectAll($collector); + + RecipeRunner::processRecipe($recipe); + $config = $this->config('system.site'); + $this->assertSame('Input Test', $config->get('name')); + // There was no SITE_SLOGAN environment variable, so it should have been + // set to an empty string. + $this->assertSame('', $config->get('slogan')); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php index 62f14df4d202..64b4c17869f5 100644 --- a/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Recipe/RecipeValidationTest.php @@ -761,6 +761,36 @@ extra: YAML, NULL, ]; + yield 'input env variable name is not a string' => [ + <<<YAML +name: Bad input +input: + bad_news: + data_type: string + description: 'Bad default definition' + default: + source: env + env: -40 +YAML, + [ + '[input][bad_news][default][env]' => ['This value should be of type string.'], + ], + ]; + yield 'input env variable name is empty' => [ + <<<YAML +name: Bad input +input: + bad_news: + data_type: string + description: 'Bad default definition' + default: + source: env + env: '' +YAML, + [ + '[input][bad_news][default][env]' => ['This value should not be blank.'], + ], + ]; } /** diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/DeprecatedElementTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/DeprecatedElementTest.php index 09931b10f3a3..16c0a7fe12bf 100644 --- a/core/tests/Drupal/KernelTests/Core/Render/Element/DeprecatedElementTest.php +++ b/core/tests/Drupal/KernelTests/Core/Render/Element/DeprecatedElementTest.php @@ -43,7 +43,8 @@ class DeprecatedElementTest extends KernelTestBase { ], $info_manager->getInfo('deprecated_extends_form')); // Ensure the constructor is triggering a deprecation error. - $previous_error_handler = set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) { + $previous_error_handler = get_error_handler(); + set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) { // Convert deprecation error into a catchable exception. if ($severity === E_USER_DEPRECATED) { throw new \ErrorException($message, 0, $severity, $file, $line); @@ -84,7 +85,8 @@ class DeprecatedElementTest extends KernelTestBase { * Test use of static methods trigger deprecations. */ public function testDeprecatedStaticMethods(): void { - $previous_error_handler = set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) { + $previous_error_handler = get_error_handler(); + set_error_handler(function ($severity, $message, $file, $line) use (&$previous_error_handler) { // Convert deprecation error into a catchable exception. if ($severity === E_USER_DEPRECATED) { throw new \ErrorException($message, 0, $severity, $file, $line); diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/LegacyStatusReportTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/LegacyStatusReportTest.php new file mode 100644 index 000000000000..93470153f326 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Render/Element/LegacyStatusReportTest.php @@ -0,0 +1,27 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Render\Element; + +use Drupal\Core\Render\Element\StatusReport; +use Drupal\KernelTests\KernelTestBase; + +/** + * Tests the status report element legacy methods. + * + * @group Render + * @group legacy + */ +class LegacyStatusReportTest extends KernelTestBase { + + /** + * Tests the getSeverities() method deprecation. + */ + public function testGetSeveritiesDeprecation(): void { + $this->expectDeprecation('Calling Drupal\Core\Render\Element\StatusReport::getSeverities() is deprecated in drupal:11.2.0 and is removed from in drupal:12.0.0. There is no replacement. See https://www.drupal.org/node/3410939'); + $severities = StatusReport::getSeverities(); + $this->assertIsArray($severities); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php index 7ad8afa75be6..c300cd4c019f 100644 --- a/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php +++ b/core/tests/Drupal/KernelTests/Core/Render/Element/PluginAlterTest.php @@ -23,7 +23,7 @@ class PluginAlterTest extends KernelTestBase { $info_manager = $this->container->get('plugin.manager.element_info'); $this->assertArrayHasKey('weight', $info_manager->getDefinitions()); - // @see element_info_test_element_plugin_alter() + // @see ElementInfoTestHooks::elementPluginAlter(). $this->container->get('state')->set('hook_element_plugin_alter:remove_weight', TRUE); // The definition will be cached. $this->assertArrayHasKey('weight', $info_manager->getDefinitions()); @@ -33,4 +33,27 @@ class PluginAlterTest extends KernelTestBase { $this->assertArrayNotHasKey('weight', $info_manager->getDefinitions()); } + /** + * Tests hook_element_plugin_alter(). + */ + public function testPluginClassSwap(): void { + $info_manager = $this->container->get('plugin.manager.element_info'); + $test_details = [ + '#type' => 'details', + '#title' => 'Title', + '#description' => 'Description', + '#open' => TRUE, + ]; + + // @see ElementInfoTestHooks::elementPluginAlter(). + $expected = [ + 'class' => 'Drupal\element_info_test\Render\Element\Details', + 'provider' => 'element_info_test', + 'id' => 'details', + ]; + $this->assertEquals($expected, $info_manager->getDefinitions()['details']); + \Drupal::service('renderer')->renderRoot($test_details); + $this->assertArrayHasKey('#custom', $test_details); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/StatusReportTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/StatusReportTest.php new file mode 100644 index 000000000000..90e4a9c36be7 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Render/Element/StatusReportTest.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Render\Element; + +use Drupal\Core\Extension\Requirement\RequirementSeverity; +use Drupal\Core\Render\Element\StatusReport; +use Drupal\KernelTests\KernelTestBase; + +include_once \DRUPAL_ROOT . '/core/includes/install.inc'; + +/** + * Tests the status report element. + * + * @group Render + * @group legacy + */ +class StatusReportTest extends KernelTestBase { + + /** + * Tests the status report element. + */ + public function testPreRenderGroupRequirements(): void { + $element = [ + '#priorities' => [ + 'error', + 'warning', + 'checked', + 'ok', + ], + '#requirements' => [ + 'foo' => [ + 'title' => 'Foo', + 'severity' => RequirementSeverity::Info, + ], + 'baz' => [ + 'title' => 'Baz', + 'severity' => RequirementSeverity::Warning, + ], + 'wiz' => [ + 'title' => 'Wiz', + 'severity' => RequirementSeverity::Error, + ], + 'bar' => [ + 'title' => 'Bar', + 'severity' => RequirementSeverity::OK, + ], + 'legacy' => [ + 'title' => 'Legacy', + 'severity' => \REQUIREMENT_OK, + ], + ], + ]; + + $this->expectDeprecation('Calling Drupal\Core\Render\Element\StatusReport::preRenderGroupRequirements() with an array of $requirements with \'severity\' with values not of type Drupal\Core\Extension\Requirement\RequirementSeverity enums is deprecated in drupal:11.2.0 and is required in drupal:12.0.0. See https://www.drupal.org/node/3410939'); + + $element = StatusReport::preRenderGroupRequirements($element); + $groups = $element['#grouped_requirements']; + + $errors = $groups['error']; + $this->assertEquals('Errors found', (string) $errors['title']); + $this->assertEquals('error', $errors['type']); + $errorItems = $errors['items']; + $this->assertCount(1, $errorItems); + $this->assertArrayHasKey('wiz', $errorItems); + + $warnings = $groups['warning']; + $this->assertEquals('Warnings found', (string) $warnings['title']); + $this->assertEquals('warning', $warnings['type']); + $warningItems = $warnings['items']; + $this->assertCount(1, $warningItems); + $this->assertArrayHasKey('baz', $warningItems); + + $checked = $groups['checked']; + $this->assertEquals('Checked', (string) $checked['title']); + $this->assertEquals('checked', $checked['type']); + $checkedItems = $checked['items']; + $this->assertCount(3, $checkedItems); + $this->assertArrayHasKey('foo', $checkedItems); + $this->assertArrayHasKey('bar', $checkedItems); + $this->assertArrayHasKey('legacy', $checkedItems); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php index 00b7948f2a81..e36da16d9028 100644 --- a/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php +++ b/core/tests/Drupal/KernelTests/Core/Render/Element/WeightTest.php @@ -8,6 +8,7 @@ use Drupal\Core\Form\FormState; use Drupal\Core\Render\Element\Number; use Drupal\Core\Render\Element\Select; use Drupal\Core\Render\Element\Weight; +use Drupal\Core\Render\ElementInfoManagerInterface; use Drupal\element_info_test\ElementInfoTestNumberBuilder; use Drupal\KernelTests\KernelTestBase; @@ -40,7 +41,7 @@ class WeightTest extends KernelTestBase { $form_state = new FormState(); $complete_form = []; - $element_object = new Weight([], 'weight', []); + $element_object = new Weight([], 'weight', [], elementInfoManager: $this->createStub(ElementInfoManagerInterface::class)); $info = $element_object->getInfo(); $element += $info; diff --git a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php new file mode 100644 index 000000000000..a8697cc84968 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiFindAllClassFilesTest.php @@ -0,0 +1,71 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Test; + +use Drupal\Core\Test\PhpUnitTestDiscovery; +use Drupal\Core\Test\TestDiscovery; +use Drupal\KernelTests\KernelTestBase; +use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; + +/** + * Tests ::findAllClassFiles() between TestDiscovery and PhpUnitTestDiscovery. + * + * PhpUnitTestDiscovery uses PHPUnit API to build the list of test classes, + * while TestDiscovery uses Drupal legacy code. + */ +#[CoversClass(PhpUnitTestDiscovery::class)] +#[Group('TestSuites')] +#[Group('Test')] +#[Group('#slow')] +class PhpUnitApiFindAllClassFilesTest extends KernelTestBase { + + /** + * Checks that Drupal legacy and PHPUnit API based discoveries are equal. + */ + #[DataProvider('argumentsProvider')] + #[IgnoreDeprecations] + public function testEquality(?string $extension = NULL, ?string $directory = NULL): void { + // PHPUnit discovery. + $configurationFilePath = $this->container->getParameter('app.root') . \DIRECTORY_SEPARATOR . 'core'; + // @todo once PHPUnit 10 is no longer used, remove the condition. + // @see https://www.drupal.org/project/drupal/issues/3497116 + if (RunnerVersion::getMajor() >= 11) { + $configurationFilePath .= \DIRECTORY_SEPARATOR . '.phpunit-next.xml'; + } + $phpUnitTestDiscovery = PhpUnitTestDiscovery::instance()->setConfigurationFilePath($configurationFilePath); + $phpUnitList = $phpUnitTestDiscovery->findAllClassFiles($extension, $directory); + + // Legacy TestDiscovery. + $testDiscovery = new TestDiscovery( + $this->container->getParameter('app.root'), + $this->container->get('class_loader') + ); + $internalList = $testDiscovery->findAllClassFiles($extension, $directory); + + // Downgrade results to make them comparable, working around bugs and + // additions. + // 1. TestDiscovery discovers non-test classes that PHPUnit does not. + $internalList = array_intersect_key($internalList, $phpUnitList); + + $this->assertEquals($internalList, $phpUnitList); + } + + /** + * Provides test data to ::testEquality. + */ + public static function argumentsProvider(): \Generator { + yield 'All tests' => []; + yield 'Extension: system' => ['extension' => 'system']; + yield 'Extension: system, directory' => [ + 'extension' => 'system', + 'directory' => 'core/modules/system/tests/src', + ]; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php new file mode 100644 index 000000000000..caedbc0d2b62 --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitApiGetTestClassesTest.php @@ -0,0 +1,108 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Test; + +use Drupal\Core\Test\PhpUnitTestDiscovery; +use Drupal\Core\Test\TestDiscovery; +use Drupal\KernelTests\KernelTestBase; +use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; + +/** + * Tests ::getTestClasses() between TestDiscovery and PhpPUnitTestDiscovery. + * + * PhpPUnitTestDiscovery uses PHPUnit API to build the list of test classes, + * while TestDiscovery uses Drupal legacy code. + */ +#[CoversClass(PhpUnitTestDiscovery::class)] +#[Group('TestSuites')] +#[Group('Test')] +#[Group('#slow')] +class PhpUnitApiGetTestClassesTest extends KernelTestBase { + + /** + * Checks that Drupal legacy and PHPUnit API based discoveries are equal. + */ + #[DataProvider('argumentsProvider')] + #[IgnoreDeprecations] + public function testEquality(array $suites, ?string $extension = NULL, ?string $directory = NULL): void { + // PHPUnit discovery. + $configurationFilePath = $this->container->getParameter('app.root') . \DIRECTORY_SEPARATOR . 'core'; + // @todo once PHPUnit 10 is no longer used, remove the condition. + // @see https://www.drupal.org/project/drupal/issues/3497116 + if (RunnerVersion::getMajor() >= 11) { + $configurationFilePath .= \DIRECTORY_SEPARATOR . '.phpunit-next.xml'; + } + $phpUnitTestDiscovery = PhpUnitTestDiscovery::instance()->setConfigurationFilePath($configurationFilePath); + $phpUnitList = $phpUnitTestDiscovery->getTestClasses($extension, $suites, $directory); + + // Legacy TestDiscovery. + $testDiscovery = new TestDiscovery( + $this->container->getParameter('app.root'), + $this->container->get('class_loader') + ); + $internalList = $testDiscovery->getTestClasses($extension, $suites, $directory); + + // Downgrade results to make them comparable, working around bugs and + // additions. + // 1. Remove TestDiscovery empty groups. + $internalList = array_filter($internalList); + // 2. Remove TestDiscovery '##no-group-annotations' group. + unset($internalList['##no-group-annotations']); + // 3. Remove 'file' and 'tests_count' keys from PHPUnit results. + foreach ($phpUnitList as &$group) { + foreach ($group as &$testClass) { + unset($testClass['file']); + unset($testClass['tests_count']); + } + } + // 4. Remove from PHPUnit results groups not found by TestDiscovery. + $phpUnitList = array_intersect_key($phpUnitList, $internalList); + // 5. Remove from PHPUnit groups classes not found by TestDiscovery. + foreach ($phpUnitList as $groupName => &$group) { + $group = array_intersect_key($group, $internalList[$groupName]); + } + // 6. Remove from PHPUnit test classes groups not found by TestDiscovery. + foreach ($phpUnitList as $groupName => &$group) { + foreach ($group as $testClassName => &$testClass) { + $testClass['groups'] = array_intersect_key($testClass['groups'], $internalList[$groupName][$testClassName]['groups']); + } + } + + $this->assertEquals($internalList, $phpUnitList); + } + + /** + * Provides test data to ::testEquality. + */ + public static function argumentsProvider(): \Generator { + yield 'All tests' => ['suites' => []]; + yield 'Testsuite: functional-javascript' => ['suites' => ['PHPUnit-FunctionalJavascript']]; + yield 'Testsuite: functional' => ['suites' => ['PHPUnit-Functional']]; + yield 'Testsuite: kernel' => ['suites' => ['PHPUnit-Kernel']]; + yield 'Testsuite: unit' => ['suites' => ['PHPUnit-Unit']]; + yield 'Testsuite: unit-component' => ['suites' => ['PHPUnit-Unit-Component']]; + yield 'Testsuite: build' => ['suites' => ['PHPUnit-Build']]; + yield 'Extension: system' => ['suites' => [], 'extension' => 'system']; + yield 'Extension: system, testsuite: unit' => [ + 'suites' => ['PHPUnit-Unit'], + 'extension' => 'system', + ]; + yield 'Extension: system, directory' => [ + 'suites' => [], + 'extension' => 'system', + 'directory' => 'core/modules/system/tests/src', + ]; + yield 'Extension: system, testsuite: unit, directory' => [ + 'suites' => ['PHPUnit-Unit'], + 'extension' => 'system', + 'directory' => 'core/modules/system/tests/src', + ]; + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php index 705981f75079..345cc9282d2e 100644 --- a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php +++ b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php @@ -7,8 +7,8 @@ namespace Drupal\KernelTests\Core\Test; use Drupal\Core\Test\TestDiscovery; use Drupal\KernelTests\KernelTestBase; use Drupal\TestTools\PhpUnitCompatibility\RunnerVersion; -use PHPUnit\TextUI\Configuration\Builder; -use PHPUnit\TextUI\Configuration\TestSuiteBuilder; +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use Symfony\Component\Process\Process; /** @@ -22,11 +22,11 @@ use Symfony\Component\Process\Process; * list thus generated, with the list generated by * \Drupal\Core\Test\TestDiscovery, which is used by run-tests.sh, to ensure * both methods will run the same tests, - * - * @group TestSuites - * @group Test - * @group #slow */ +#[Group('TestSuites')] +#[Group('Test')] +#[Group('#slow')] +#[IgnoreDeprecations] class PhpUnitTestDiscoveryTest extends KernelTestBase { private const TEST_LIST_MISMATCH_MESSAGE = @@ -76,11 +76,19 @@ class PhpUnitTestDiscoveryTest extends KernelTestBase { $internalList = array_unique($internalList); asort($internalList); + // Location of PHPUnit configuration file. + $configurationFilePath = $this->root . \DIRECTORY_SEPARATOR . 'core'; + // @todo once PHPUnit 10 is no longer used, remove the condition. + // @see https://www.drupal.org/project/drupal/issues/3497116 + if (RunnerVersion::getMajor() >= 11) { + $configurationFilePath .= \DIRECTORY_SEPARATOR . '.phpunit-next.xml'; + } + // PHPUnit's test discovery - via CLI execution. $process = new Process([ 'vendor/bin/phpunit', '--configuration', - 'core', + $configurationFilePath, '--list-tests-xml', $this->xmlOutputFile, ], $this->root); @@ -112,27 +120,6 @@ class PhpUnitTestDiscoveryTest extends KernelTestBase { // Check against Drupal's discovery. $this->assertEquals(implode("\n", $phpUnitClientList), implode("\n", $internalList), self::TEST_LIST_MISMATCH_MESSAGE); - - // @todo once PHPUnit 10 is no longer used re-enable the rest of the test. - // @see https://www.drupal.org/project/drupal/issues/3497116 - if (RunnerVersion::getMajor() >= 11) { - $this->markTestIncomplete('On PHPUnit 11+ the test triggers warnings due to phpunit.xml setup. Re-enable in https://www.drupal.org/project/drupal/issues/3497116.'); - } - - // PHPUnit's test discovery - via API. - $phpUnitConfiguration = (new Builder())->build(['--configuration', 'core']); - $phpUnitTestSuite = (new TestSuiteBuilder())->build($phpUnitConfiguration); - $phpUnitApiList = []; - foreach ($phpUnitTestSuite->tests() as $testSuite) { - foreach ($testSuite->tests() as $test) { - $phpUnitApiList[] = $test->name(); - } - } - asort($phpUnitApiList); - - // Check against Drupal's discovery. - $this->assertEquals(implode("\n", $phpUnitApiList), implode("\n", $internalList), self::TEST_LIST_MISMATCH_MESSAGE); - } } diff --git a/core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php b/core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php index f2f9bd16a04e..cd4fdc1258c8 100644 --- a/core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php +++ b/core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\KernelTests\Core\Updater; +use Drupal\Core\Extension\Requirement\RequirementSeverity; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\KernelTests\KernelTestBase; @@ -27,7 +28,7 @@ class UpdateRequirementsTest extends KernelTestBase { 'title' => 'UpdateError', 'value' => 'None', 'description' => 'Update Error.', - 'severity' => REQUIREMENT_ERROR, + 'severity' => RequirementSeverity::Error, ]; $requirements = update_check_requirements()['test.update.error']; $this->assertEquals($testRequirements, $requirements); @@ -36,7 +37,7 @@ class UpdateRequirementsTest extends KernelTestBase { 'title' => 'UpdateWarning', 'value' => 'None', 'description' => 'Update Warning.', - 'severity' => REQUIREMENT_WARNING, + 'severity' => RequirementSeverity::Warning, ]; $alterRequirements = update_check_requirements()['test.update.error.alter']; $this->assertEquals($testAlterRequirements, $alterRequirements); diff --git a/core/tests/Drupal/KernelTests/Core/Validation/UriHostValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Validation/UriHostValidatorTest.php new file mode 100644 index 000000000000..aa845c50b1da --- /dev/null +++ b/core/tests/Drupal/KernelTests/Core/Validation/UriHostValidatorTest.php @@ -0,0 +1,74 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\KernelTests\Core\Validation; + +use Drupal\KernelTests\KernelTestBase; + +/** + * Tests the UriHost validator. + * + * @group Validation + */ +class UriHostValidatorTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['config_test']; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + + $this->installConfig('config_test'); + } + + /** + * @see \Drupal\Core\Validation\Plugin\Validation\Constraint\UriHostConstraint + */ + public function testUriHost(): void { + $typed_config_manager = \Drupal::service('config.typed'); + /** @var \Drupal\Core\Config\Schema\TypedConfigInterface $typed_config */ + $typed_config = $typed_config_manager->get('config_test.validation'); + + // Test valid names. + $typed_config->get('host')->setValue('example.com'); + $this->assertCount(0, $typed_config->validate()); + + $typed_config->get('host')->setValue('example.com.'); + $this->assertCount(0, $typed_config->validate()); + + $typed_config->get('host')->setValue('default'); + $this->assertCount(0, $typed_config->validate()); + + // Test invalid names. + $typed_config->get('host')->setValue('.example.com'); + $this->assertCount(1, $typed_config->validate()); + + // Test valid IPv6 literals. + $typed_config->get('host')->setValue('[::1]'); + $this->assertCount(0, $typed_config->validate()); + + $typed_config->get('host')->setValue('[2001:DB8::]'); + $this->assertCount(0, $typed_config->validate()); + + $typed_config->get('host')->setValue('[2001:db8:dd54:4473:bd6e:52db:10b3:4abe]'); + $this->assertCount(0, $typed_config->validate()); + + // Test invalid IPv6 literals. + $typed_config->get('host')->setValue('::1'); + $this->assertCount(1, $typed_config->validate()); + + // Test valid IPv4 addresses. + $typed_config->get('host')->setValue('127.0.0.1'); + $this->assertCount(0, $typed_config->validate()); + + $typed_config->get('host')->setValue('192.0.2.254'); + $this->assertCount(0, $typed_config->validate()); + } + +} diff --git a/core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php index 7c7d6aa0e39b..5889553a1491 100644 --- a/core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php +++ b/core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php @@ -44,48 +44,4 @@ class UuidValidatorTest extends KernelTestBase { $this->assertCount(1, $typed_config->validate()); } - /** - * @see \Drupal\Core\Validation\Plugin\Validation\Constraint\UriHostConstraint - */ - public function testUriHost(): void { - $typed_config_manager = \Drupal::service('config.typed'); - /** @var \Drupal\Core\Config\Schema\TypedConfigInterface $typed_config */ - $typed_config = $typed_config_manager->get('config_test.validation'); - - // Test valid names. - $typed_config->get('host')->setValue('example.com'); - $this->assertCount(0, $typed_config->validate()); - - $typed_config->get('host')->setValue('example.com.'); - $this->assertCount(0, $typed_config->validate()); - - $typed_config->get('host')->setValue('default'); - $this->assertCount(0, $typed_config->validate()); - - // Test invalid names. - $typed_config->get('host')->setValue('.example.com'); - $this->assertCount(1, $typed_config->validate()); - - // Test valid IPv6 literals. - $typed_config->get('host')->setValue('[::1]'); - $this->assertCount(0, $typed_config->validate()); - - $typed_config->get('host')->setValue('[2001:DB8::]'); - $this->assertCount(0, $typed_config->validate()); - - $typed_config->get('host')->setValue('[2001:db8:dd54:4473:bd6e:52db:10b3:4abe]'); - $this->assertCount(0, $typed_config->validate()); - - // Test invalid IPv6 literals. - $typed_config->get('host')->setValue('::1'); - $this->assertCount(1, $typed_config->validate()); - - // Test valid IPv4 addresses. - $typed_config->get('host')->setValue('127.0.0.1'); - $this->assertCount(0, $typed_config->validate()); - - $typed_config->get('host')->setValue('192.0.2.254'); - $this->assertCount(0, $typed_config->validate()); - } - } diff --git a/core/tests/Drupal/KernelTests/KernelTestBase.php b/core/tests/Drupal/KernelTests/KernelTestBase.php index 34aca08f15a9..c45e937d371e 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBase.php +++ b/core/tests/Drupal/KernelTests/KernelTestBase.php @@ -439,7 +439,7 @@ abstract class KernelTestBase extends TestCase implements ServiceProviderInterfa throw new \Exception('There is no database connection so no tests can be run. You must provide a SIMPLETEST_DB environment variable to run PHPUnit based functional tests outside of run-tests.sh. See https://www.drupal.org/node/2116263#skipped-tests for more information.'); } else { - $database = Database::convertDbUrlToConnectionInfo($db_url, $this->root, TRUE); + $database = Database::convertDbUrlToConnectionInfo($db_url, TRUE); Database::addConnectionInfo('default', 'default', $database); } diff --git a/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php b/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php index e8dfc3be6f45..828b4ab54ec0 100644 --- a/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php +++ b/core/tests/Drupal/KernelTests/KernelTestBaseDatabaseDriverModuleTest.php @@ -25,7 +25,7 @@ class KernelTestBaseDatabaseDriverModuleTest extends KernelTestBase { throw new \Exception('There is no database connection so no tests can be run. You must provide a SIMPLETEST_DB environment variable to run PHPUnit based functional tests outside of run-tests.sh. See https://www.drupal.org/node/2116263#skipped-tests for more information.'); } else { - $database = Database::convertDbUrlToConnectionInfo($db_url, $this->root); + $database = Database::convertDbUrlToConnectionInfo($db_url); if (in_array($database['driver'], ['mysql', 'pgsql'])) { // Change the used database driver to the one provided by the module diff --git a/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php b/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php index 02521bdda2f8..2f8b7309b520 100644 --- a/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php +++ b/core/tests/Drupal/KernelTests/Scripts/TestSiteApplicationTest.php @@ -286,7 +286,7 @@ class TestSiteApplicationTest extends KernelTestBase { * The database key of the added connection. */ protected function addTestDatabase($db_prefix): string { - $database = Database::convertDbUrlToConnectionInfo(getenv('SIMPLETEST_DB'), $this->root); + $database = Database::convertDbUrlToConnectionInfo(getenv('SIMPLETEST_DB')); $database['prefix'] = $db_prefix; $target = __CLASS__ . $db_prefix; Database::addConnectionInfo($target, 'default', $database); |