summaryrefslogtreecommitdiffstatshomepage
path: root/core/tests/Drupal/KernelTests
diff options
context:
space:
mode:
Diffstat (limited to 'core/tests/Drupal/KernelTests')
-rw-r--r--core/tests/Drupal/KernelTests/Components/ComponentInFormTest.php155
-rw-r--r--core/tests/Drupal/KernelTests/Components/ComponentNegotiatorTest.php1
-rw-r--r--core/tests/Drupal/KernelTests/Components/ComponentNodeVisitorTest.php36
-rw-r--r--core/tests/Drupal/KernelTests/Components/ComponentPluginManagerCachedDiscoveryTest.php38
-rw-r--r--core/tests/Drupal/KernelTests/Components/ComponentRenderTest.php32
-rw-r--r--core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php1
-rw-r--r--core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php46
-rw-r--r--core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTagTest.php31
-rw-r--r--core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php4
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php6
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/FetchLegacyTest.php22
-rw-r--r--core/tests/Drupal/KernelTests/Core/Database/TransactionTest.php1278
-rw-r--r--core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php1036
-rw-r--r--core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateTest.php980
-rw-r--r--core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php2
-rw-r--r--core/tests/Drupal/KernelTests/Core/Extension/LegacyRequirementSeverityTest.php86
-rw-r--r--core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php30
-rw-r--r--core/tests/Drupal/KernelTests/Core/Render/Element/LegacyStatusReportTest.php27
-rw-r--r--core/tests/Drupal/KernelTests/Core/Render/Element/StatusReportTest.php85
-rw-r--r--core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php15
-rw-r--r--core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php5
-rw-r--r--core/tests/Drupal/KernelTests/Core/Validation/UriHostValidatorTest.php74
-rw-r--r--core/tests/Drupal/KernelTests/Core/Validation/UuidValidatorTest.php44
23 files changed, 2979 insertions, 1055 deletions
diff --git a/core/tests/Drupal/KernelTests/Components/ComponentInFormTest.php b/core/tests/Drupal/KernelTests/Components/ComponentInFormTest.php
new file mode 100644
index 00000000000..147b647d2da
--- /dev/null
+++ b/core/tests/Drupal/KernelTests/Components/ComponentInFormTest.php
@@ -0,0 +1,155 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\KernelTests\Components;
+
+use Drupal\Core\Form\FormInterface;
+use Drupal\Core\Form\FormState;
+use Drupal\Core\Form\FormStateInterface;
+
+/**
+ * Tests the correct rendering of components in form.
+ *
+ * @group sdc
+ */
+class ComponentInFormTest extends ComponentKernelTestBase implements FormInterface {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $modules = [
+ 'system',
+ 'sdc_test',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ protected static $themes = ['sdc_theme_test'];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId(): string {
+ return 'component_in_form_test';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state): array {
+ $form['normal'] = [
+ '#type' => 'textfield',
+ '#title' => 'Normal form element',
+ '#default_value' => 'fake 1',
+ ];
+
+ // We want to test form elements inside a component, itself inside a
+ // component.
+ $form['banner'] = [
+ '#type' => 'component',
+ '#component' => 'sdc_test:my-banner',
+ '#props' => [
+ 'ctaText' => 'Click me!',
+ 'ctaHref' => 'https://www.example.org',
+ 'ctaTarget' => '',
+ ],
+ 'banner_body' => [
+ '#type' => 'component',
+ '#component' => 'sdc_theme_test:my-card',
+ '#props' => [
+ 'header' => 'Card header',
+ ],
+ 'card_body' => [
+ 'foo' => [
+ '#type' => 'textfield',
+ '#title' => 'Textfield in component',
+ '#default_value' => 'fake 2',
+ ],
+ 'bar' => [
+ '#type' => 'select',
+ '#title' => 'Select in component',
+ '#options' => [
+ 'option_1' => 'Option 1',
+ 'option_2' => 'Option 2',
+ ],
+ '#empty_option' => 'Empty option',
+ '#default_value' => 'option_1',
+ ],
+ ],
+ ],
+ ];
+
+ $form['actions'] = [
+ '#type' => 'actions',
+ 'submit' => [
+ '#type' => 'submit',
+ '#value' => 'Submit',
+ ],
+ ];
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state): void {
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state): void {
+ // Check that submitted data are present (set with #default_value).
+ $data = [
+ 'normal' => 'fake 1',
+ 'foo' => 'fake 2',
+ 'bar' => 'option_1',
+ ];
+ foreach ($data as $key => $value) {
+ $this->assertSame($value, $form_state->getValue($key));
+ }
+ }
+
+ /**
+ * Tests that fields validation messages are sorted in the fields order.
+ */
+ public function testFormRenderingAndSubmission(): void {
+ /** @var \Drupal\Core\Form\FormBuilderInterface $form_builder */
+ $form_builder = \Drupal::service('form_builder');
+ /** @var \Drupal\Core\Render\RendererInterface $renderer */
+ $renderer = \Drupal::service('renderer');
+ $form = $form_builder->getForm($this);
+
+ // Test form structure after being processed.
+ $this->assertTrue($form['normal']['#processed'], 'The normal textfield should have been processed.');
+ $this->assertTrue($form['banner']['banner_body']['card_body']['bar']['#processed'], 'The textfield inside component should have been processed.');
+ $this->assertTrue($form['banner']['banner_body']['card_body']['foo']['#processed'], 'The select inside component should have been processed.');
+ $this->assertTrue($form['actions']['submit']['#processed'], 'The submit button should have been processed.');
+
+ // Test form rendering.
+ $markup = $renderer->renderRoot($form);
+ $this->setRawContent($markup);
+
+ // Ensure form elements are rendered once.
+ $this->assertCount(1, $this->cssSelect('input[name="normal"]'), 'The normal textfield should have been rendered once.');
+ $this->assertCount(1, $this->cssSelect('input[name="foo"]'), 'The foo textfield should have been rendered once.');
+ $this->assertCount(1, $this->cssSelect('select[name="bar"]'), 'The bar select should have been rendered once.');
+
+ // Check the position of the form elements in the DOM.
+ $paths = [
+ '//form/div[1]/input[@name="normal"]',
+ '//form/div[2][@data-component-id="sdc_test:my-banner"]/div[2][@class="component--my-banner--body"]/div[1][@data-component-id="sdc_theme_test:my-card"]/div[1][@class="component--my-card__body"]/div[1]/input[@name="foo"]',
+ '//form/div[2][@data-component-id="sdc_test:my-banner"]/div[2][@class="component--my-banner--body"]/div[1][@data-component-id="sdc_theme_test:my-card"]/div[1][@class="component--my-card__body"]/div[2]/select[@name="bar"]',
+ ];
+ foreach ($paths as $path) {
+ $this->assertNotEmpty($this->xpath($path), 'There should be a result with the path: ' . $path . '.');
+ }
+
+ // Test form submission. Assertions are in submitForm().
+ $form_state = new FormState();
+ $form_builder->submitForm($this, $form_state);
+ }
+
+}
diff --git a/core/tests/Drupal/KernelTests/Components/ComponentNegotiatorTest.php b/core/tests/Drupal/KernelTests/Components/ComponentNegotiatorTest.php
index eb38e858f0d..ffdf5e96bf4 100644
--- a/core/tests/Drupal/KernelTests/Components/ComponentNegotiatorTest.php
+++ b/core/tests/Drupal/KernelTests/Components/ComponentNegotiatorTest.php
@@ -72,6 +72,7 @@ class ComponentNegotiatorTest extends ComponentKernelTestBase {
'#type' => 'inline_template',
'#template' => "{{ include('sdc_theme_test:my-card') }}",
'#context' => ['header' => 'Foo bar'],
+ '#variant' => 'horizontal',
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertNotEmpty($crawler->filter('#sdc-wrapper .component--my-card--replaced__body'));
diff --git a/core/tests/Drupal/KernelTests/Components/ComponentNodeVisitorTest.php b/core/tests/Drupal/KernelTests/Components/ComponentNodeVisitorTest.php
index 6b72fa745ea..d4869433072 100644
--- a/core/tests/Drupal/KernelTests/Components/ComponentNodeVisitorTest.php
+++ b/core/tests/Drupal/KernelTests/Components/ComponentNodeVisitorTest.php
@@ -22,6 +22,9 @@ class ComponentNodeVisitorTest extends ComponentKernelTestBase {
*/
protected static $themes = ['sdc_theme_test'];
+ const DEBUG_COMPONENT_ID_PATTERN = '/<!-- ([\n\s\S]*) Component start: ([\SA-Za-z+-:]+) -->/';
+ const DEBUG_VARIANT_ID_PATTERN = '/<!-- [\n\s\S]* with variant: "([\SA-Za-z+-]+)" -->/';
+
/**
* Test that other visitors can modify Twig nodes.
*/
@@ -36,4 +39,37 @@ class ComponentNodeVisitorTest extends ComponentKernelTestBase {
$this->assertTrue(TRUE);
}
+ /**
+ * Test debug output for sdc components with component id and variant.
+ */
+ public function testDebugRendersComponentStartWithVariant(): void {
+ // Enable twig theme debug to ensure that any
+ // changes to theme debugging format force checking
+ // that the auto paragraph filter continues to be applied
+ // correctly.
+ $twig = \Drupal::service('twig');
+ $twig->enableDebug();
+
+ $build = [
+ '#type' => 'component',
+ '#component' => 'sdc_theme_test:my-card',
+ '#variant' => 'vertical',
+ '#props' => [
+ 'header' => 'My header',
+ ],
+ '#slots' => [
+ 'card_body' => 'Foo bar',
+ ],
+ ];
+ $crawler = $this->renderComponentRenderArray($build);
+ $content = $crawler->html();
+
+ $matches = [];
+ \preg_match_all(self::DEBUG_COMPONENT_ID_PATTERN, $content, $matches);
+ $this->assertSame($matches[2][0], 'sdc_theme_test:my-card');
+
+ \preg_match_all(self::DEBUG_VARIANT_ID_PATTERN, $content, $matches);
+ $this->assertSame($matches[1][0], 'vertical');
+ }
+
}
diff --git a/core/tests/Drupal/KernelTests/Components/ComponentPluginManagerCachedDiscoveryTest.php b/core/tests/Drupal/KernelTests/Components/ComponentPluginManagerCachedDiscoveryTest.php
new file mode 100644
index 00000000000..1b012c9f726
--- /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/Components/ComponentRenderTest.php b/core/tests/Drupal/KernelTests/Components/ComponentRenderTest.php
index 70a5e804af7..944d6cb145b 100644
--- a/core/tests/Drupal/KernelTests/Components/ComponentRenderTest.php
+++ b/core/tests/Drupal/KernelTests/Components/ComponentRenderTest.php
@@ -96,7 +96,7 @@ class ComponentRenderTest extends ComponentKernelTestBase {
$build = [
'#type' => 'inline_template',
'#context' => ['content' => $content],
- '#template' => "{% embed 'sdc_theme_test:my-card' with { header: 'Card header', content: content } only %}{% block card_body %}This is a card with a CTA {{ include('sdc_test:my-cta', { text: content.heading, href: 'https://www.example.org', target: '_blank' }, with_context = false) }}{% endblock %}{% endembed %}",
+ '#template' => "{% embed 'sdc_theme_test:my-card' with { variant: 'horizontal', header: 'Card header', content: content } only %}{% block card_body %}This is a card with a CTA {{ include('sdc_test:my-cta', { text: content.heading, href: 'https://www.example.org', target: '_blank' }, with_context = false) }}{% endblock %}{% endembed %}",
];
$crawler = $this->renderComponentRenderArray($build);
$this->assertNotEmpty($crawler->filter('#sdc-wrapper [data-component-id="sdc_theme_test:my-card"] h2.component--my-card__header:contains("Card header")'));
@@ -353,6 +353,36 @@ class ComponentRenderTest extends ComponentKernelTestBase {
}
/**
+ * Ensure that components variants render.
+ */
+ public function testVariants(): void {
+ $build = [
+ '#type' => 'component',
+ '#component' => 'sdc_test:my-cta',
+ '#variant' => 'primary',
+ '#props' => [
+ 'text' => 'Test link',
+ ],
+ ];
+ $crawler = $this->renderComponentRenderArray($build);
+ $this->assertNotEmpty($crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta"][data-component-variant="primary"][class*="my-cta-primary"]'));
+
+ // If there were an existing prop named variant, we don't override that for BC reasons.
+ $build = [
+ '#type' => 'component',
+ '#component' => 'sdc_test:my-cta-with-variant-prop',
+ '#variant' => 'tertiary',
+ '#props' => [
+ 'text' => 'Test link',
+ 'variant' => 'secondary',
+ ],
+ ];
+ $crawler = $this->renderComponentRenderArray($build);
+ $this->assertEmpty($crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta-with-variant-prop"][data-component-variant="tertiary"]'));
+ $this->assertNotEmpty($crawler->filter('#sdc-wrapper a[data-component-id="sdc_test:my-cta-with-variant-prop"][data-component-variant="secondary"]'));
+ }
+
+ /**
* Ensures some key aspects of the plugin definition are correctly computed.
*
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
diff --git a/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php b/core/tests/Drupal/KernelTests/Config/Schema/MappingTest.php
index 3ca62dcced5..f375f6f3dd6 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/ResolvedLibraryDefinitionsFilesMatchTest.php b/core/tests/Drupal/KernelTests/Core/Asset/ResolvedLibraryDefinitionsFilesMatchTest.php
index 340dcc28649..15c97bea71f 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 c2bff123236..c1ff7e24406 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/ConfigEntityValidationTestBase.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
index ad556d07e0a..cb0b3086b14 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigEntityValidationTestBase.php
@@ -260,7 +260,7 @@ abstract class ConfigEntityValidationTestBase extends KernelTestBase {
],
[
'dependencies.module.0' => [
- 'This value is not valid.',
+ 'This value is not a valid extension name.',
"Module 'invalid-module-name' is not installed.",
],
],
@@ -290,7 +290,7 @@ abstract class ConfigEntityValidationTestBase extends KernelTestBase {
],
[
'dependencies.theme.0' => [
- 'This value is not valid.',
+ 'This value is not a valid extension name.',
"Theme 'invalid-theme-name' is not installed.",
],
],
diff --git a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php
index c361c7af959..cbda6e3d7f7 100644
--- a/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php
+++ b/core/tests/Drupal/KernelTests/Core/Database/DriverSpecificTransactionTestBase.php
@@ -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 6e28787f764..3bfaa18889a 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 00000000000..f40a977f6f4
--- /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/Entity/EntityDefinitionUpdateMultipleTypesTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDefinitionUpdateMultipleTypesTest.php
new file mode 100644
index 00000000000..5e708fbbc2f
--- /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 (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 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 6437e594ee8..d184892a976 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/ExtensionNameConstraintTest.php b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php
index 568a3d3ca4f..4aaff77dbaa 100644
--- a/core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Extension/ExtensionNameConstraintTest.php
@@ -39,7 +39,7 @@ class ExtensionNameConstraintTest extends KernelTestBase {
$data->setValue('invalid-name');
$violations = $data->validate();
$this->assertCount(1, $violations);
- $this->assertSame('This value is not valid.', (string) $violations->get(0)->getMessage());
+ $this->assertSame('This value is not a valid extension name.', (string) $violations->get(0)->getMessage());
}
}
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 00000000000..020ef21c227
--- /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/Recipe/InputTest.php b/core/tests/Drupal/KernelTests/Core/Recipe/InputTest.php
index ffc5a85fdb0..fdfa189b880 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,32 @@ 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);
+ }
+
}
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 00000000000..93470153f32
--- /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/StatusReportTest.php b/core/tests/Drupal/KernelTests/Core/Render/Element/StatusReportTest.php
new file mode 100644
index 00000000000..90e4a9c36be
--- /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/Test/PhpUnitTestDiscoveryTest.php b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php
index 6d7fcaeed9a..705981f7507 100644
--- a/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Test/PhpUnitTestDiscoveryTest.php
@@ -6,6 +6,7 @@ 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 Symfony\Component\Process\Process;
@@ -96,14 +97,28 @@ class PhpUnitTestDiscoveryTest extends KernelTestBase {
$phpUnitXmlList = new \DOMDocument();
$phpUnitXmlList->loadXML(file_get_contents($this->xmlOutputFile));
$phpUnitClientList = [];
+ // Try PHPUnit 10 format first.
+ // @todo remove once PHPUnit 10 is no longer used.
foreach ($phpUnitXmlList->getElementsByTagName('testCaseClass') as $node) {
$phpUnitClientList[] = $node->getAttribute('name');
}
+ // If empty, try PHPUnit 11+ format.
+ if (empty($phpUnitClientList)) {
+ foreach ($phpUnitXmlList->getElementsByTagName('testClass') as $node) {
+ $phpUnitClientList[] = $node->getAttribute('name');
+ }
+ }
asort($phpUnitClientList);
// 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);
diff --git a/core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php b/core/tests/Drupal/KernelTests/Core/Updater/UpdateRequirementsTest.php
index f2f9bd16a04..cd4fdc1258c 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 00000000000..aa845c50b1d
--- /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 7c7d6aa0e39..5889553a149 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());
- }
-
}