class BulkDeleteTest

Unit test class for field bulk delete and batch purge functionality.

Hierarchy

Expanded class hierarchy of BulkDeleteTest

File

drupal/core/modules/field/lib/Drupal/field/Tests/BulkDeleteTest.php, line 17
Definition of Drupal\field\Tests\BulkDeleteTest.

Namespace

Drupal\field\Tests
View source
class BulkDeleteTest extends FieldUnitTestBase {
  protected $field;
  public static function getInfo() {
    return array(
      'name' => 'Field bulk delete tests',
      'description' => 'Bulk delete fields and instances, and clean up afterwards.',
      'group' => 'Field API',
    );
  }

  /**
   * Converts the passed entities to partially created ones.
   *
   * This replicates the partial entities created in field_purge_data_batch(),
   * which only have the ids and the to be deleted field defined.
   *
   * @param $entities
   *   An array of entities of type test_entity.
   * @param $field_name
   *   A field name whose data should be copied from $entities into the returned
   *   partial entities.
   * @return
   *   An array of partial entities corresponding to $entities.
   */
  protected function convertToPartialEntities($entities, $field_name) {
    $partial_entities = array();
    foreach ($entities as $id => $entity) {

      // Re-create the entity to match what is expected
      // _field_create_entity_from_ids().
      $ids = (object) array(
        'entity_id' => $entity->ftid,
        'revision_id' => $entity->ftvid,
        'bundle' => $entity->fttype,
        'entity_type' => 'test_entity',
      );
      $partial_entities[$id] = _field_create_entity_from_ids($ids);
      $partial_entities[$id]->{$field_name} = $entity->{$field_name};
    }
    return $partial_entities;
  }

  /**
   * 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().
   */
  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) {

          // $entity is sometimes the first and sometimes the second argument.
          if ($actual_arguments[0] == $argument || $actual_arguments[1] == $argument) {
            $found = TRUE;
            break;
          }
        }
        $this
          ->assertTrue($found, "{$hook}() was called on expected argument");
      }
    }
  }
  function setUp() {
    parent::setUp();
    $this->fields = array();
    $this->instances = array();
    $this->entities = array();
    $this->entities_by_bundles = array();

    // Create two bundles.
    $this->bundles = array(
      'bb_1' => 'bb_1',
      'bb_2' => 'bb_2',
    );
    foreach ($this->bundles as $name => $desc) {
      field_test_create_bundle($name, $desc);
    }

    // Create two fields.
    $field = array(
      'field_name' => 'bf_1',
      'type' => 'test_field',
      'cardinality' => 1,
    );
    $this->fields[] = field_create_field($field);
    $field = array(
      'field_name' => 'bf_2',
      'type' => 'test_field',
      'cardinality' => 4,
    );
    $this->fields[] = field_create_field($field);

    // For each bundle, create an instance of each field, and 10
    // entities with values for each field.
    $id = 1;
    $this->entity_type = 'test_entity';
    foreach ($this->bundles as $bundle) {
      foreach ($this->fields as $field) {
        $instance = array(
          'field_name' => $field['field_name'],
          'entity_type' => $this->entity_type,
          'bundle' => $bundle,
        );
        $this->instances[] = field_create_instance($instance);
      }
      for ($i = 0; $i < 10; $i++) {
        $entity = field_test_create_entity($id, $id, $bundle);
        foreach ($this->fields as $field) {
          $entity->{$field['field_name']}[Language::LANGCODE_NOT_SPECIFIED] = $this
            ->_generateTestFieldValues($field['cardinality']);
        }
        $entity
          ->save();
        $id++;
      }
    }
    $this->entities = entity_load_multiple($this->entity_type, range(1, $id));
    foreach ($this->entities as $entity) {

      // Also keep track of the entities per bundle.
      $this->entities_by_bundles[$entity->fttype][$entity->ftid] = $entity;
    }
  }

  /**
   * Verify that deleting an instance leaves the field data items in
   * the database and that the appropriate Field API functions can
   * operate on the deleted data and instance.
   *
   * This tests how EntityFieldQuery interacts with
   * field_delete_instance() and could be moved to FieldCrudTestCase,
   * but depends on this class's setUp().
   */
  function testDeleteFieldInstance() {
    $bundle = reset($this->bundles);
    $field = reset($this->fields);
    $field_name = $field['field_name'];
    $factory = \Drupal::service('entity.query');

    // There are 10 entities of this bundle.
    $found = $factory
      ->get('test_entity')
      ->condition('fttype', $bundle)
      ->execute();
    $this
      ->assertEqual(count($found), 10, 'Correct number of entities found before deleting');

    // Delete the instance.
    $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle);
    field_delete_instance($instance);

    // The instance still exists, deleted.
    $instances = field_read_instances(array(
      'field_id' => $field['uuid'],
      'deleted' => TRUE,
    ), array(
      'include_deleted' => TRUE,
      'include_inactive' => TRUE,
    ));
    $this
      ->assertEqual(count($instances), 1, 'There is one deleted instance');
    $this
      ->assertEqual($instances[0]['bundle'], $bundle, 'The deleted instance is for the correct bundle');

    // There are 0 entities of this bundle with non-deleted data.
    $found = $factory
      ->get('test_entity')
      ->condition('fttype', $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('test_entity')
      ->condition('fttype', $bundle)
      ->condition("{$field_name}.deleted", 1)
      ->sort('ftid')
      ->execute();
    $ids = (object) array(
      'entity_type' => 'test_entity',
      'bundle' => $bundle,
    );
    $entities = array();
    foreach ($found as $entity_id) {
      $ids->entity_id = $entity_id;
      $entities[$entity_id] = _field_create_entity_from_ids($ids);
    }
    field_attach_load($this->entity_type, $entities, FIELD_LOAD_CURRENT, array(
      'field_id' => $field['uuid'],
      'deleted' => TRUE,
    ));
    $this
      ->assertEqual(count($found), 10, 'Correct number of entities found after deleting');
    foreach ($entities as $id => $entity) {
      $this
        ->assertEqual($this->entities[$id]->{$field['field_name']}, $entity->{$field['field_name']}, "Entity {$id} with deleted data loaded correctly");
    }
  }

  /**
   * Verify that field data items and instances are purged when an
   * instance is deleted.
   */
  function testPurgeInstance() {

    // Start recording hook invocations.
    field_test_memorize();
    $bundle = reset($this->bundles);
    $field = reset($this->fields);

    // Delete the instance.
    $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle);
    field_delete_instance($instance);

    // 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('test_entity')
        ->condition('fttype', $bundle)
        ->condition($field['field_name'] . '.deleted', 1)
        ->execute();
      $this
        ->assertEqual(count($found), $count, 'Correct number of entities found after purging 2');
    }

    // Check hooks invocations.
    // - hook_field_load() (multiple hook) should have been called on all
    // entities by pairs of two.
    // - hook_field_delete() should have been called once for each entity in the
    // bundle.
    $actual_hooks = field_test_memorize();
    $hooks = array();
    $entities = $this
      ->convertToPartialEntities($this->entities_by_bundles[$bundle], $field['field_name']);
    foreach (array_chunk($entities, $batch_size, TRUE) as $chunk_entity) {
      $hooks['field_test_field_load'][] = $chunk_entity;
    }
    foreach ($entities as $entity) {
      $hooks['field_test_field_delete'][] = $entity;
    }
    $this
      ->checkHooksInvocations($hooks, $actual_hooks);

    // The instance still exists, deleted.
    $instances = field_read_instances(array(
      'field_id' => $field['uuid'],
      'deleted' => TRUE,
    ), array(
      'include_deleted' => TRUE,
      'include_inactive' => TRUE,
    ));
    $this
      ->assertEqual(count($instances), 1, 'There is one deleted instance');

    // Purge the instance.
    field_purge_batch($batch_size);

    // The instance is gone.
    $instances = field_read_instances(array(
      'field_id' => $field['uuid'],
      'deleted' => TRUE,
    ), array(
      'include_deleted' => TRUE,
      'include_inactive' => TRUE,
    ));
    $this
      ->assertEqual(count($instances), 0, 'The instance is gone');

    // The field still exists, not deleted, because it has a second instance.
    $fields = field_read_fields(array(
      'uuid' => $field['uuid'],
    ), array(
      'include_deleted' => TRUE,
      'include_inactive' => TRUE,
    ));
    $this
      ->assertTrue(isset($fields[$field['uuid']]), 'The field exists and is not deleted');
  }

  /**
   * Verify that fields are preserved and purged correctly as multiple
   * instances are deleted and purged.
   */
  function testPurgeField() {

    // Start recording hook invocations.
    field_test_memorize();
    $field = reset($this->fields);

    // Delete the first instance.
    $bundle = reset($this->bundles);
    $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle);
    field_delete_instance($instance);

    // Assert that hook_field_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.
    // - hook_field_load() (multiple hook) should have been called once, for all
    // entities in the bundle.
    // - hook_field_delete() should have been called once for each entity in the
    // bundle.
    $actual_hooks = field_test_memorize();
    $hooks = array();
    $entities = $this
      ->convertToPartialEntities($this->entities_by_bundles[$bundle], $field['field_name']);
    $hooks['field_test_field_load'][] = $entities;
    $hooks['field_test_field_delete'] = $entities;
    $this
      ->checkHooksInvocations($hooks, $actual_hooks);

    // Purge again to purge the instance.
    field_purge_batch(0);

    // The field still exists, not deleted.
    $fields = field_read_fields(array(
      'uuid' => $field['uuid'],
    ), array(
      'include_deleted' => TRUE,
    ));
    $this
      ->assertTrue(isset($fields[$field['uuid']]) && !$fields[$field['uuid']]->deleted, 'The field exists and is not deleted');

    // Delete the second instance.
    $bundle = next($this->bundles);
    $instance = field_info_instance($this->entity_type, $field['field_name'], $bundle);
    field_delete_instance($instance);

    // Assert that hook_field_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 = array();
    $entities = $this
      ->convertToPartialEntities($this->entities_by_bundles[$bundle], $field['field_name']);
    $hooks['field_test_field_load'][] = $entities;
    $hooks['field_test_field_delete'] = $entities;
    $this
      ->checkHooksInvocations($hooks, $actual_hooks);

    // The field still exists, deleted.
    $fields = field_read_fields(array(
      'uuid' => $field['uuid'],
    ), array(
      'include_deleted' => TRUE,
    ));
    $this
      ->assertTrue(isset($fields[$field['uuid']]) && $fields[$field['uuid']]->deleted, 'The field exists and is deleted');

    // Purge again to purge the instance and the field.
    field_purge_batch(0);

    // The field is gone.
    $fields = field_read_fields(array(
      'uuid' => $field['uuid'],
    ), array(
      'include_deleted' => TRUE,
      'include_inactive' => TRUE,
    ));
    $this
      ->assertEqual(count($fields), 0, 'The field is purged.');
  }

}

Members

Name Modifiers Type Descriptionsort descending Overrides
BulkDeleteTest::getInfo public static function
BulkDeleteTest::$field protected property
DrupalUnitTestBase::$moduleFiles private property
DrupalUnitTestBase::$themeFiles private property
DrupalUnitTestBase::$themeData private property
UnitTestBase::$configDirectories protected property
TestBase::$setupDatabasePrefix protected property
TestBase::$setupEnvironment protected property
DrupalUnitTestBase::$keyValueFactory protected property A KeyValueMemoryFactory instance to use when building the container.
FieldUnitTestBase::$content protected property A string for assert raw and text helper methods.
FieldUnitTestBase::assertFieldValues function Assert that a field has the expected values in an entity.
TestBase::$assertions protected property Assertions thrown in that test case.
TestBase::settingsSet protected function Changes in memory settings.
TestBase::changeDatabasePrefix protected function Changes the database connection to the prefixed one.
TestBase::assertFalse protected function Check to see if a value is false (an empty string, 0, NULL, or FALSE).
TestBase::assertTrue protected function Check to see if a value is not false (not an empty string, 0, NULL, or FALSE).
TestBase::assertNotNull protected function Check to see if a value is not NULL.
TestBase::assertNull protected function Check to see if a value is NULL.
TestBase::assertEqual protected function Check to see if two values are equal.
TestBase::assertIdentical protected function Check to see if two values are identical.
TestBase::assertNotEqual protected function Check to see if two values are not equal.
TestBase::assertNotIdentical protected function Check to see if two values are not identical.
TestBase::checkRequirements protected function Checks the matching requirements for Test. 4
TestBase::assertIdenticalObject protected function Checks to see if two objects are identical.
TestBase::generatePermutations public static function Converts a list of possible parameters into a stack of permutations.
BulkDeleteTest::convertToPartialEntities protected function Converts the passed entities to partially created ones.
TestBase::copyConfig public function Copies configuration objects from source storage to target storage.
FieldUnitTestBase::createFieldWithInstance function Create a field and an instance of it.
TestBase::prepareConfigDirectories protected function Create and set new configuration directories. 1
TestBase::$results public property Current results of this test case.
TestBase::getAssertionCall protected function Cycles through backtrace until the first non-assertion method is found.
TestBase::deleteAssert public static function Delete an assertion record by message ID.
DrupalUnitTestBase::tearDown protected function Deletes created files, database tables, and reverts all environment changes. Overrides TestBase::tearDown 2
TestBase::$verboseDirectory protected property Directory where verbose output files are put.
DrupalUnitTestBase::disableModules protected function Disables modules for this test.
DrupalUnitTestBase::enableModules protected function Enables modules for this test.
TestBase::filePreDeleteCallback public static function Ensures test files are deletable within file_unmanaged_delete_recursive().
TestBase::fail protected function Fire an assertion that is always negative.
TestBase::pass protected function Fire an assertion that is always positive.
TestBase::error protected function Fire an error assertion. 1
TestBase::$setup protected property Flag to indicate whether the test has been set up.
FieldUnitTestBase::_generateTestFieldValues function Generate random values for a field_test field.
TestBase::prepareDatabasePrefix protected function Generates a database prefix for running tests.
TestBase::randomObject public static function Generates a random PHP object.
TestBase::randomName public static function Generates a random string containing letters and numbers.
TestBase::randomString public static function Generates a random string of ASCII characters of codes 32 to 126.
TestBase::errorHandler public function Handle errors during test runs.
TestBase::exceptionHandler protected function Handle exceptions.
TestBase::$verboseId protected property Incrementing identifier for verbose output filenames.
DrupalUnitTestBase::installSchema protected function Installs a specific table from a module schema definition.
DrupalUnitTestBase::installConfig protected function Installs default configuration for a given list of modules.
TestBase::assert protected function Internal helper: stores the assert.
TestBase::verbose protected function Logs verbose message in a text file.
FieldUnitTestBase::$modules public static property Modules to enable. Overrides DrupalUnitTestBase::$modules 15
DrupalUnitTestBase::__construct function Overrides \Drupal\simpletest\UnitTestBase::__construct(). Overrides UnitTestBase::__construct
FieldUnitTestBase::assertRaw protected function Pass if the raw text IS found in set string.
FieldUnitTestBase::assertNoRaw protected function Pass if the raw text IS NOT found in set string.
FieldUnitTestBase::assertText protected function Pass if the text IS found in set string.
FieldUnitTestBase::assertNoText protected function Pass if the text IS NOT found in set string.
TestBase::prepareEnvironment protected function Prepares the current environment for running the test.
TestBase::rebuildContainer protected function Rebuild drupal_container(). 1
TestBase::configImporter public function Returns a ConfigImporter object to import test importing of configuration. 1
TestBase::getDatabaseConnection public static function Returns the database connection to the site running Simpletest.
TestBase::run public function Run all tests in this class.
TestBase::$verboseClassName protected property Safe class name for use in verbose output filenames.
BulkDeleteTest::setUp function Set the default field storage backend for fields created during tests. Overrides FieldUnitTestBase::setUp
DrupalUnitTestBase::containerBuild public function Sets up the base service container for this test. 1
TestBase::insertAssert public static function Store an assertion from outside the testing context.
BulkDeleteTest::checkHooksInvocations function Tests that the expected hooks have been invoked on the expected entities.
TestBase::$configImporter protected property The config importer that can used in a test. 1
TestBase::$databasePrefix protected property The database prefix of this test run.
TestBase::$container protected property The dependency injection container used in the test. 1
TestBase::$originalPrefix protected property The original database prefix when running inside Simpletest.
TestBase::$originalFileDirectory protected property The original file directory, before it was changed for testing purposes.
TestBase::$public_files_directory protected property The public file directory for the test environment.
TestBase::$originalSettings protected property The settings array.
TestBase::$testId protected property The test run ID.
TestBase::$skipClasses protected property This class is skipped when looking for the source of an assertion.
TestBase::$timeLimit protected property Time limit for the test.
TestBase::$verbose protected property TRUE if verbose debugging is enabled.
TestBase::$verboseDirectoryUrl protected property URL to the verbose output file directory.
BulkDeleteTest::testDeleteFieldInstance function Verify that deleting an instance leaves the field data items in the database and that the appropriate Field API functions can operate on the deleted data and instance.
BulkDeleteTest::testPurgeInstance function Verify that field data items and instances are purged when an instance is deleted.
BulkDeleteTest::testPurgeField function Verify that fields are preserved and purged correctly as multiple instances are deleted and purged.
TestBase::$dieOnFail public property Whether to die in case any test assertion fails.