diff core/modules/field/tests/src/Kernel/BulkDeleteTest.php @ 0:4c8ae668cc8c

Initial import (non-working)
author Chris Cannam
date Wed, 29 Nov 2017 16:09:58 +0000
parents
children 129ea1e6d783
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core/modules/field/tests/src/Kernel/BulkDeleteTest.php	Wed Nov 29 16:09:58 2017 +0000
@@ -0,0 +1,452 @@
+<?php
+
+namespace Drupal\Tests\field\Kernel;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\field\Entity\FieldConfig;
+use Drupal\field\Entity\FieldStorageConfig;
+
+/**
+ * Bulk delete storages and fields, and clean up afterwards.
+ *
+ * @group field
+ */
+class BulkDeleteTest extends FieldKernelTestBase {
+
+  /**
+   * The fields to use in this test.
+   *
+   * @var array
+   */
+  protected $fieldStorages;
+
+  /**
+   * The entities to use in this test.
+   *
+   * @var array
+   */
+  protected $entities;
+
+  /**
+   * The entities to use in this test, keyed by bundle.
+   *
+   * @var array
+   */
+  protected $entitiesByBundles;
+
+  /**
+   * The bundles for the entities used in this test.
+   *
+   * @var array
+   */
+  protected $bundles;
+
+  /**
+   * The entity type to be used in the test classes.
+   *
+   * @var string
+   */
+  protected $entityTypeId = 'entity_test';
+
+  /**
+   * Tests that the expected hooks have been invoked on the expected entities.
+   *
+   * @param $expected_hooks
+   *   An array keyed by hook name, with one entry per expected invocation.
+   *   Each entry is the value of the "$entity" parameter the hook is expected
+   *   to have been passed.
+   * @param $actual_hooks
+   *   The array of actual hook invocations recorded by field_test_memorize().
+   */
+  public function checkHooksInvocations($expected_hooks, $actual_hooks) {
+    foreach ($expected_hooks as $hook => $invocations) {
+      $actual_invocations = $actual_hooks[$hook];
+
+      // Check that the number of invocations is correct.
+      $this->assertEqual(count($actual_invocations), count($invocations), "$hook() was called the expected number of times.");
+
+      // Check that the hook was called for each expected argument.
+      foreach ($invocations as $argument) {
+        $found = FALSE;
+        foreach ($actual_invocations as $actual_arguments) {
+          // The argument we are looking for is either an array of entities as
+          // the second argument or a single entity object as the first.
+          if ($argument instanceof EntityInterface && $actual_arguments[0]->id() == $argument->id()) {
+            $found = TRUE;
+            break;
+          }
+          // In case of an array, compare the array size and make sure it
+          // contains the same elements.
+          elseif (is_array($argument) && count($actual_arguments[1]) == count($argument) && count(array_diff_key($actual_arguments[1], $argument)) == 0) {
+            $found = TRUE;
+            break;
+          }
+        }
+        $this->assertTrue($found, "$hook() was called on expected argument");
+      }
+    }
+  }
+
+  protected function setUp() {
+    parent::setUp();
+
+    $this->fieldStorages = [];
+    $this->entities = [];
+    $this->entitiesByBundles = [];
+
+    // Create two bundles.
+    $this->bundles = ['bb_1' => 'bb_1', 'bb_2' => 'bb_2'];
+    foreach ($this->bundles as $name => $desc) {
+      entity_test_create_bundle($name, $desc);
+    }
+
+    // Create two field storages.
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => 'bf_1',
+      'entity_type' => $this->entityTypeId,
+      'type' => 'test_field',
+      'cardinality' => 1
+    ]);
+    $field_storage->save();
+    $this->fieldStorages[] = $field_storage;
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => 'bf_2',
+      'entity_type' => $this->entityTypeId,
+      'type' => 'test_field',
+      'cardinality' => 4
+    ]);
+    $field_storage->save();
+    $this->fieldStorages[] = $field_storage;
+
+    // For each bundle, create each field, and 10 entities with values for the
+    // fields.
+    foreach ($this->bundles as $bundle) {
+      foreach ($this->fieldStorages as $field_storage) {
+        FieldConfig::create([
+          'field_storage' => $field_storage,
+          'bundle' => $bundle,
+        ])->save();
+      }
+      for ($i = 0; $i < 10; $i++) {
+        $entity = $this->container->get('entity_type.manager')
+          ->getStorage($this->entityTypeId)
+          ->create(['type' => $bundle]);
+        foreach ($this->fieldStorages as $field_storage) {
+          $entity->{$field_storage->getName()}->setValue($this->_generateTestFieldValues($field_storage->getCardinality()));
+        }
+        $entity->save();
+      }
+    }
+    $this->entities = $this->container->get('entity_type.manager')
+      ->getStorage($this->entityTypeId)->loadMultiple();
+    foreach ($this->entities as $entity) {
+      // This test relies on the entities having stale field definitions
+      // so that the deleted field can be accessed on them. Access the field
+      // now, so that they are always loaded.
+      $entity->bf_1->value;
+
+      // Also keep track of the entities per bundle.
+      $this->entitiesByBundles[$entity->bundle()][$entity->id()] = $entity;
+    }
+  }
+
+  /**
+   * Verify that deleting a field leaves the field data items in the database
+   * and that the appropriate Field API functions can operate on the deleted
+   * data and field definition.
+   *
+   * This tests how EntityFieldQuery interacts with field deletion and could be
+   * moved to FieldCrudTestCase, but depends on this class's setUp().
+   */
+  public function testDeleteField() {
+    $bundle = reset($this->bundles);
+    $field_storage = reset($this->fieldStorages);
+    $field_name = $field_storage->getName();
+    $factory = \Drupal::service('entity.query');
+
+    // There are 10 entities of this bundle.
+    $found = $factory->get('entity_test')
+      ->condition('type', $bundle)
+      ->execute();
+    $this->assertEqual(count($found), 10, 'Correct number of entities found before deleting');
+
+    // Delete the field.
+    $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
+    $field->delete();
+
+    // The field still exists, deleted.
+    $fields = entity_load_multiple_by_properties('field_config', ['field_storage_uuid' => $field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE]);
+    $this->assertEqual(count($fields), 1, 'There is one deleted field');
+    $field = $fields[$field->uuid()];
+    $this->assertEqual($field->getTargetBundle(), $bundle, 'The deleted field is for the correct bundle');
+
+    // Check that the actual stored content did not change during delete.
+    $storage = \Drupal::entityManager()->getStorage($this->entityTypeId);
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
+    $table_mapping = $storage->getTableMapping();
+    $table = $table_mapping->getDedicatedDataTableName($field_storage);
+    $column = $table_mapping->getFieldColumnName($field_storage, 'value');
+    $result = db_select($table, 't')
+      ->fields('t')
+      ->execute();
+    foreach ($result as $row) {
+      $this->assertEqual($this->entities[$row->entity_id]->{$field_name}->value, $row->$column);
+    }
+
+    // There are 0 entities of this bundle with non-deleted data.
+    $found = $factory->get('entity_test')
+      ->condition('type', $bundle)
+      ->condition("$field_name.deleted", 0)
+      ->execute();
+    $this->assertFalse($found, 'No entities found after deleting');
+
+    // There are 10 entities of this bundle when deleted fields are allowed, and
+    // their values are correct.
+    $found = $factory->get('entity_test')
+      ->condition('type', $bundle)
+      ->condition("$field_name.deleted", 1)
+      ->sort('id')
+      ->execute();
+    $this->assertEqual(count($found), 10, 'Correct number of entities found after deleting');
+    $this->assertFalse(array_diff($found, array_keys($this->entities)));
+  }
+
+  /**
+   * Tests that recreating a field with the name as a deleted field works.
+   */
+  public function testPurgeWithDeletedAndActiveField() {
+    $bundle = reset($this->bundles);
+    // Create another field storage.
+    $field_name = 'bf_3';
+    $deleted_field_storage = FieldStorageConfig::create([
+      'field_name' => $field_name,
+      'entity_type' => $this->entityTypeId,
+      'type' => 'test_field',
+      'cardinality' => 1
+    ]);
+    $deleted_field_storage->save();
+    // Create the field.
+    FieldConfig::create([
+      'field_storage' => $deleted_field_storage,
+      'bundle' => $bundle,
+    ])->save();
+
+    for ($i = 0; $i < 20; $i++) {
+      $entity = $this->container->get('entity_type.manager')
+        ->getStorage($this->entityTypeId)
+        ->create(['type' => $bundle]);
+      $entity->{$field_name}->setValue($this->_generateTestFieldValues(1));
+      $entity->save();
+    }
+
+    // Delete the field.
+    $deleted_field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
+    $deleted_field->delete();
+    $deleted_field_uuid = $deleted_field->uuid();
+
+    // Reload the field storage.
+    $field_storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $deleted_field_storage->uuid(), 'include_deleted' => TRUE]);
+    $deleted_field_storage = reset($field_storages);
+
+    // Create the field again.
+    $field_storage = FieldStorageConfig::create([
+      'field_name' => $field_name,
+      'entity_type' => $this->entityTypeId,
+      'type' => 'test_field',
+      'cardinality' => 1
+    ]);
+    $field_storage->save();
+    FieldConfig::create([
+      'field_storage' => $field_storage,
+      'bundle' => $bundle,
+    ])->save();
+
+    // The field still exists, deleted, with the same field name.
+    $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $deleted_field_uuid, 'include_deleted' => TRUE]);
+    $this->assertTrue(isset($fields[$deleted_field_uuid]) && $fields[$deleted_field_uuid]->isDeleted(), 'The field exists and is deleted');
+    $this->assertTrue(isset($fields[$deleted_field_uuid]) && $fields[$deleted_field_uuid]->getName() == $field_name);
+
+    for ($i = 0; $i < 10; $i++) {
+      $entity = $this->container->get('entity_type.manager')
+        ->getStorage($this->entityTypeId)
+        ->create(['type' => $bundle]);
+      $entity->{$field_name}->setValue($this->_generateTestFieldValues(1));
+      $entity->save();
+    }
+
+    // Check that the two field storages have different tables.
+    $storage = \Drupal::entityManager()->getStorage($this->entityTypeId);
+    /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
+    $table_mapping = $storage->getTableMapping();
+    $deleted_table_name = $table_mapping->getDedicatedDataTableName($deleted_field_storage, TRUE);
+    $active_table_name = $table_mapping->getDedicatedDataTableName($field_storage);
+
+    field_purge_batch(50);
+
+    // Ensure the new field still has its table and the deleted one has been
+    // removed.
+    $this->assertTrue(\Drupal::database()->schema()->tableExists($active_table_name));
+    $this->assertFalse(\Drupal::database()->schema()->tableExists($deleted_table_name));
+
+    // The field has been removed from the system.
+    $fields = entity_load_multiple_by_properties('field_config', ['field_storage_uuid' => $deleted_field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE]);
+    $this->assertEqual(count($fields), 0, 'The field is gone');
+
+    // Verify there are still 10 entries in the main table.
+    $count = \Drupal::database()
+      ->select('entity_test__' . $field_name, 'f')
+      ->fields('f', ['entity_id'])
+      ->condition('bundle', $bundle)
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $this->assertEqual($count, 10);
+  }
+
+  /**
+   * Verify that field data items and fields are purged when a field storage is
+   * deleted.
+   */
+  public function testPurgeField() {
+    // Start recording hook invocations.
+    field_test_memorize();
+
+    $bundle = reset($this->bundles);
+    $field_storage = reset($this->fieldStorages);
+    $field_name = $field_storage->getName();
+
+    // Delete the field.
+    $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
+    $field->delete();
+
+    // No field hooks were called.
+    $mem = field_test_memorize();
+    $this->assertEqual(count($mem), 0, 'No field hooks were called');
+
+    $batch_size = 2;
+    for ($count = 8; $count >= 0; $count -= $batch_size) {
+      // Purge two entities.
+      field_purge_batch($batch_size);
+
+      // There are $count deleted entities left.
+      $found = \Drupal::entityQuery('entity_test')
+        ->condition('type', $bundle)
+        ->condition($field_name . '.deleted', 1)
+        ->execute();
+      $this->assertEqual(count($found), $count, 'Correct number of entities found after purging 2');
+    }
+
+    // Check hooks invocations.
+    // FieldItemInterface::delete() should have been called once for each entity in the
+    // bundle.
+    $actual_hooks = field_test_memorize();
+    $hooks = [];
+    $entities = $this->entitiesByBundles[$bundle];
+    foreach ($entities as $id => $entity) {
+      $hooks['field_test_field_delete'][] = $entity;
+    }
+    $this->checkHooksInvocations($hooks, $actual_hooks);
+
+    // The field still exists, deleted.
+    $fields = entity_load_multiple_by_properties('field_config', ['field_storage_uuid' => $field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE]);
+    $this->assertEqual(count($fields), 1, 'There is one deleted field');
+
+    // Purge the field.
+    field_purge_batch($batch_size);
+
+    // The field is gone.
+    $fields = entity_load_multiple_by_properties('field_config', ['field_storage_uuid' => $field_storage->uuid(), 'deleted' => TRUE, 'include_deleted' => TRUE]);
+    $this->assertEqual(count($fields), 0, 'The field is gone');
+
+    // The field storage still exists, not deleted, because it has a second
+    // field.
+    $storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $field_storage->uuid(), 'include_deleted' => TRUE]);
+    $this->assertTrue(isset($storages[$field_storage->uuid()]), 'The field storage exists and is not deleted');
+  }
+
+  /**
+   * Verify that field storages are preserved and purged correctly as multiple
+   * fields are deleted and purged.
+   */
+  public function testPurgeFieldStorage() {
+    // Start recording hook invocations.
+    field_test_memorize();
+
+    $field_storage = reset($this->fieldStorages);
+    $field_name = $field_storage->getName();
+
+    // Delete the first field.
+    $bundle = reset($this->bundles);
+    $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
+    $field->delete();
+
+    // Assert that FieldItemInterface::delete() was not called yet.
+    $mem = field_test_memorize();
+    $this->assertEqual(count($mem), 0, 'No field hooks were called.');
+
+    // Purge the data.
+    field_purge_batch(10);
+
+    // Check hooks invocations.
+    // FieldItemInterface::delete() should have been called once for each entity in the
+    // bundle.
+    $actual_hooks = field_test_memorize();
+    $hooks = [];
+    $entities = $this->entitiesByBundles[$bundle];
+    foreach ($entities as $id => $entity) {
+      $hooks['field_test_field_delete'][] = $entity;
+    }
+    $this->checkHooksInvocations($hooks, $actual_hooks);
+
+    // The field still exists, deleted.
+    $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $field->uuid(), 'include_deleted' => TRUE]);
+    $this->assertTrue(isset($fields[$field->uuid()]) && $fields[$field->uuid()]->isDeleted(), 'The field exists and is deleted');
+
+    // Purge again to purge the field.
+    field_purge_batch(0);
+
+    // The field is gone.
+    $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $field->uuid(), 'include_deleted' => TRUE]);
+    $this->assertEqual(count($fields), 0, 'The field is purged.');
+    // The field storage still exists, not deleted.
+    $storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $field_storage->uuid(), 'include_deleted' => TRUE]);
+    $this->assertTrue(isset($storages[$field_storage->uuid()]) && !$storages[$field_storage->uuid()]->isDeleted(), 'The field storage exists and is not deleted');
+
+    // Delete the second field.
+    $bundle = next($this->bundles);
+    $field = FieldConfig::loadByName($this->entityTypeId, $bundle, $field_name);
+    $field->delete();
+
+    // Assert that FieldItemInterface::delete() was not called yet.
+    $mem = field_test_memorize();
+    $this->assertEqual(count($mem), 0, 'No field hooks were called.');
+
+    // Purge the data.
+    field_purge_batch(10);
+
+    // Check hooks invocations (same as above, for the 2nd bundle).
+    $actual_hooks = field_test_memorize();
+    $hooks = [];
+    $entities = $this->entitiesByBundles[$bundle];
+    foreach ($entities as $id => $entity) {
+      $hooks['field_test_field_delete'][] = $entity;
+    }
+    $this->checkHooksInvocations($hooks, $actual_hooks);
+
+    // The field and the storage still exist, deleted.
+    $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $field->uuid(), 'include_deleted' => TRUE]);
+    $this->assertTrue(isset($fields[$field->uuid()]) && $fields[$field->uuid()]->isDeleted(), 'The field exists and is deleted');
+    $storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $field_storage->uuid(), 'include_deleted' => TRUE]);
+    $this->assertTrue(isset($storages[$field_storage->uuid()]) && $storages[$field_storage->uuid()]->isDeleted(), 'The field storage exists and is deleted');
+
+    // Purge again to purge the field and the storage.
+    field_purge_batch(0);
+
+    // The field and the storage are gone.
+    $fields = entity_load_multiple_by_properties('field_config', ['uuid' => $field->uuid(), 'include_deleted' => TRUE]);
+    $this->assertEqual(count($fields), 0, 'The field is purged.');
+    $storages = entity_load_multiple_by_properties('field_storage_config', ['uuid' => $field_storage->uuid(), 'include_deleted' => TRUE]);
+    $this->assertEqual(count($storages), 0, 'The field storage is purged.');
+  }
+
+}