From 3f369f179b9f50ef1490c8e4ff8cc35938ef8f17 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Thu, 9 Jan 2014 14:19:06 +0000 Subject: [PATCH] Issue #2160345 by chx, YesCT, andypost: Clean up and test migrate executable and sql idmap. --- .../lib/Drupal/migrate/Entity/Migration.php | 27 + .../migrate/Entity/MigrationInterface.php | 5 +- .../lib/Drupal/migrate/MigrateExecutable.php | 356 +++++---- .../migrate/MigrateSkipRowException.php | 15 + .../migrate/Plugin/MigrateIdMapInterface.php | 2 +- .../migrate/Plugin/migrate/id_map/Sql.php | 26 +- .../Drupal/migrate/Tests/FakeConnection.php | 160 ++++ .../migrate/Tests/FakeDatabaseSchema.php | 14 +- .../tests/Drupal/migrate/Tests/FakeInsert.php | 71 ++ .../tests/Drupal/migrate/Tests/FakeMerge.php | 61 ++ .../tests/Drupal/migrate/Tests/FakeSelect.php | 11 +- .../Drupal/migrate/Tests/FakeStatement.php | 15 +- .../Drupal/migrate/Tests/FakeTruncate.php | 36 + .../tests/Drupal/migrate/Tests/FakeUpdate.php | 88 +++ .../migrate/Tests/MigrateExecutableTest.php | 389 ++++++++- .../MigrateExecuteableMemoryExceededTest.php | 139 ++++ .../Tests/MigrateSqlIdMapEnsureTablesTest.php | 205 +++++ .../migrate/Tests/MigrateSqlIdMapTest.php | 741 ++++++++++++++++++ .../Drupal/migrate/Tests/MigrateTestCase.php | 66 +- .../tests/Drupal/migrate/Tests/RowTest.php | 260 ++++++ .../migrate/Tests/TestMigrateExecutable.php | 231 ++++++ .../Drupal/migrate/Tests/TestSqlIdMap.php | 45 ++ .../Drupal/migrate/Tests/process/GetTest.php | 15 + .../migrate/Tests/process/IteratorTest.php | 3 +- .../Tests/process/MigrateProcessTestCase.php | 2 +- .../Tests/d6/MigrateUserRoleTest.php | 4 +- 26 files changed, 2742 insertions(+), 245 deletions(-) create mode 100644 core/modules/migrate/lib/Drupal/migrate/MigrateSkipRowException.php create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/FakeConnection.php create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/FakeInsert.php create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/FakeMerge.php create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/FakeTruncate.php create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/FakeUpdate.php create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecuteableMemoryExceededTest.php create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapEnsureTablesTest.php create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapTest.php create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/RowTest.php create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/TestMigrateExecutable.php create mode 100644 core/modules/migrate/tests/Drupal/migrate/Tests/TestSqlIdMap.php diff --git a/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php b/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php index 60df0686a45a..e572bed8d06b 100644 --- a/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php +++ b/core/modules/migrate/lib/Drupal/migrate/Entity/Migration.php @@ -174,6 +174,16 @@ class Migration extends ConfigEntityBase implements MigrationInterface { */ public $sourceRowStatus = MigrateIdMapInterface::STATUS_IMPORTED; + /** + * The ratio of the memory limit at which an operation will be interrupted. + * + * Can be overridden by a Migration subclass if one would like to push the + * envelope. Defaults to 0.85. + * + * @var float + */ + protected $memoryThreshold = 0.85; + /** * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface */ @@ -184,6 +194,23 @@ class Migration extends ConfigEntityBase implements MigrationInterface { */ public $trackLastImported = FALSE; + /** + * The ratio of the time limit at which an operation will be interrupted. + * + * Can be overridden by a Migration subclass if one would like to push the + * envelope. Defaults to 0.9. + * + * @var float + */ + public $timeThreshold = 0.90; + + /** + * The time limit when executing the migration. + * + * @var array + */ + public $limit = array(); + /** * {@inheritdoc} */ diff --git a/core/modules/migrate/lib/Drupal/migrate/Entity/MigrationInterface.php b/core/modules/migrate/lib/Drupal/migrate/Entity/MigrationInterface.php index 1fa3dad0ee7b..4999cdf87c9a 100644 --- a/core/modules/migrate/lib/Drupal/migrate/Entity/MigrationInterface.php +++ b/core/modules/migrate/lib/Drupal/migrate/Entity/MigrationInterface.php @@ -63,8 +63,9 @@ public function getSourcePlugin(); * @param array $process * A process configuration array. * - * @return array - * A list of process plugins. + * @return \Drupal\migrate\Plugin\MigrateProcessInterface[][] + * An associative array. The keys are the destination property names. Values + * are process pipelines. Each pipeline contains an array of plugins. */ public function getProcessPlugins(array $process = NULL); diff --git a/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php b/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php index 7d78cd419fad..104f187b377d 100644 --- a/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php +++ b/core/modules/migrate/lib/Drupal/migrate/MigrateExecutable.php @@ -28,14 +28,14 @@ class MigrateExecutable { * * @var int */ - protected $successes_since_feedback; + protected $successesSinceFeedback; /** * The number of rows that were successfully processed. * * @var int */ - protected $total_successes; + protected $totalSuccesses; /** * Status of one row. @@ -54,7 +54,7 @@ class MigrateExecutable { * * @var int */ - protected $total_processed; + protected $totalProcessed; /** * The queued messages not yet saved. @@ -78,14 +78,25 @@ class MigrateExecutable { protected $options; /** - * The fraction of the memory limit at which an operation will be interrupted. + * The PHP max_execution_time. * - * Can be overridden by a Migration subclass if one would like to push the - * envelope. Defaults to 85%. + * @var int + */ + protected $maxExecTime; + + /** + * The configuration values of the source. * - * @var float + * @var array */ - protected $memoryThreshold = 0.85; + protected $sourceIdValues; + + /** + * The number of rows processed since feedback was given. + * + * @var int + */ + protected $processedSinceFeedback = 0; /** * The PHP memory_limit expressed in bytes. @@ -95,42 +106,55 @@ class MigrateExecutable { protected $memoryLimit; /** - * The fraction of the time limit at which an operation will be interrupted. - * - * Can be overridden by a Migration subclass if one would like to push the - * envelope. Defaults to 90%. + * The translation manager. * - * @var float + * @var \Drupal\Core\StringTranslation\TranslationInterface */ - protected $timeThreshold = 0.90; + protected $translationManager; /** - * The PHP max_execution_time. + * The rollback action to be saved for the current row. * * @var int */ - protected $timeLimit; + public $rollbackAction; /** - * The configuration values of the source. + * An array of counts. Initially used for cache hit/miss tracking. * * @var array */ - protected $sourceIdValues; + protected $counts = array(); /** - * The number of rows processed since feedback was given. + * The maximum number of items to pass in a single call during a rollback. + * + * For use in bulkRollback(). Can be overridden in derived class constructor. * * @var int */ - protected $processed_since_feedback = 0; + protected $rollbackBatchSize = 50; /** - * The translation manager. + * The object currently being constructed. * - * @var \Drupal\Core\StringTranslation\TranslationInterface + * @var \stdClass */ - protected $translationManager; + protected $destinationValues; + + /** + * The source. + * + * @var \Drupal\migrate\Source + */ + protected $source; + + /** + * The current data row retrieved from the source. + * + * @var \stdClass + */ + protected $sourceValues; /** * Constructs a MigrateExecutable and verifies and sets the memory limit. @@ -169,6 +193,8 @@ public function __construct(MigrationInterface $migration, MigrateMessageInterfa } $this->memoryLimit = $limit; } + // Record the maximum execution time limit. + $this->maxExecTime = ini_get('max_execution_time'); } /** @@ -187,41 +213,7 @@ public function getSource() { } /** - * The rollback action to be saved for the current row. - * - * @var int - */ - public $rollbackAction; - - /** - * An array of counts. Initially used for cache hit/miss tracking. - * - * @var array - */ - protected $counts = array(); - - /** - * When performing a bulkRollback(), the maximum number of items to pass in - * a single call. Can be overridden in derived class constructor. - * - * @var int - */ - protected $rollbackBatchSize = 50; - - /** - * The object currently being constructed - * @var \stdClass - */ - protected $destinationValues; - - /** - * The current data row retrieved from the source. - * @var \stdClass - */ - protected $sourceValues; - - /** - * Perform an import operation - migrate items from source to destination. + * Performs an import operation - migrate items from source to destination. */ public function import() { $return = MigrationInterface::RESULT_COMPLETED; @@ -238,44 +230,55 @@ public function import() { array('!e' => $e->getMessage()))); return MigrationInterface::RESULT_FAILED; } + while ($source->valid()) { $row = $source->current(); if ($this->sourceIdValues = $row->getSourceIdValues()) { // Wipe old messages, and save any new messages. - $id_map->delete($row->getSourceIdValues(), TRUE); + $id_map->delete($this->sourceIdValues, TRUE); $this->saveQueuedMessages(); } - $this->processRow($row); - try { - $destination_id_values = $destination->import($row); - // @TODO handle the successful but no ID case like config. - if ($destination_id_values) { - $id_map->saveIdMapping($row, $destination_id_values, $this->sourceRowStatus, $this->rollbackAction); - $this->successes_since_feedback++; - $this->total_successes++; - } - else { - $id_map->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_FAILED, $this->rollbackAction); - if ($id_map->messageCount() == 0) { - $message = $this->t('New object was not saved, no error provided'); - $this->saveMessage($message); - $this->message->display($message); - } - } + $this->processRow($row); + $save = TRUE; } - catch (MigrateException $e) { - $this->migration->getIdMap()->saveIdMapping($row, array(), $e->getStatus(), $this->rollbackAction); - $this->saveMessage($e->getMessage(), $e->getLevel()); - $this->message->display($e->getMessage()); + catch (MigrateSkipRowException $e) { + $id_map->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_IGNORED, $this->rollbackAction); + $save = FALSE; } - catch (\Exception $e) { - $this->migration->getIdMap()->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_FAILED, $this->rollbackAction); - $this->handleException($e); + + if ($save) { + try { + $destination_id_values = $destination->import($row); + // @todo Handle the successful but no ID case like config, + // https://drupal.org/node/2160835. + if ($destination_id_values) { + $id_map->saveIdMapping($row, $destination_id_values, $this->sourceRowStatus, $this->rollbackAction); + $this->successesSinceFeedback++; + $this->totalSuccesses++; + } + else { + $id_map->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_FAILED, $this->rollbackAction); + if (!$id_map->messageCount()) { + $message = $this->t('New object was not saved, no error provided'); + $this->saveMessage($message); + $this->message->display($message); + } + } + } + catch (MigrateException $e) { + $this->migration->getIdMap()->saveIdMapping($row, array(), $e->getStatus(), $this->rollbackAction); + $this->saveMessage($e->getMessage(), $e->getLevel()); + $this->message->display($e->getMessage()); + } + catch (\Exception $e) { + $this->migration->getIdMap()->saveIdMapping($row, array(), MigrateIdMapInterface::STATUS_FAILED, $this->rollbackAction); + $this->handleException($e); + } } - $this->total_processed++; - $this->processed_since_feedback++; + $this->totalProcessed++; + $this->processedSinceFeedback++; if ($highwater_property = $this->migration->get('highwaterProperty')) { $this->migration->saveHighwater($row->getSourceProperty($highwater_property['name'])); } @@ -284,12 +287,6 @@ public function import() { unset($sourceValues, $destinationValues); $this->sourceRowStatus = MigrateIdMapInterface::STATUS_IMPORTED; - // TODO: Temporary. Remove when http://drupal.org/node/375494 is committed. - // TODO: Should be done in MigrateDestinationEntity - if (!empty($destination->entityType)) { - entity_get_controller($destination->entityType)->resetCache(); - } - if (($return = $this->checkStatus()) != MigrationInterface::RESULT_COMPLETED) { break; } @@ -316,22 +313,27 @@ public function import() { } /** - * @param Row $row + * Processes a row. + * + * @param \Drupal\migrate\Row $row * The $row to be processed. * @param array $process - * A process pipeline configuration. If not set, the top level process - * configuration in the migration entity is used. + * (optional) A process pipeline configuration. If not set, the top level + * process configuration in the migration entity is used. * @param mixed $value - * Optional initial value of the pipeline for the first destination. + * (optional) Initial value of the pipeline for the first destination. * Usually setting this is not necessary as $process typically starts with * a 'get'. This is useful only when the $process contains a single * destination and needs to access a value outside of the source. See * \Drupal\migrate\Plugin\migrate\process\Iterator::transformKey for an * example. + * + * @throws \Drupal\migrate\MigrateException */ public function processRow(Row $row, array $process = NULL, $value = NULL) { foreach ($this->migration->getProcessPlugins($process) as $destination => $plugins) { $multiple = FALSE; + /** @var $plugin \Drupal\migrate\Plugin\MigrateProcessInterface */ foreach ($plugins as $plugin) { $definition = $plugin->getPluginDefinition(); // Many plugins expect a scalar value but the current value of the @@ -353,32 +355,38 @@ public function processRow(Row $row, array $process = NULL, $value = NULL) { $multiple = $multiple || $plugin->multiple(); } } - $row->setDestinationProperty($destination, $value); + // No plugins means do not set. + if ($plugins) { + $row->setDestinationProperty($destination, $value); + } // Reset the value. $value = NULL; } } /** - * Fetch the key array for the current source record. + * Fetches the key array for the current source record. * * @return array + * The current source IDs. */ protected function currentSourceIds() { return $this->getSource()->getCurrentIds(); } /** - * Test whether we've exceeded the designated time limit. + * Tests whether we've exceeded the designated time limit. * - * @return boolean - * TRUE if the threshold is exceeded, FALSE if not. + * @return bool + * TRUE if the threshold is exceeded, FALSE if not. */ protected function timeOptionExceeded() { + // If there is no time limit, then it is not exceeded. if (!$time_limit = $this->getTimeLimit()) { return FALSE; } - $time_elapsed = time() - REQUEST_TIME; + // Calculate if the time limit is exceeded. + $time_elapsed = $this->getTimeElapsed(); if ($time_elapsed >= $time_limit) { return TRUE; } @@ -387,6 +395,12 @@ protected function timeOptionExceeded() { } } + /** + * Returns the time limit. + * + * @return null|int + * The time limit, NULL if no limit or if the units were not in seconds. + */ public function getTimeLimit() { $limit = $this->migration->get('limit'); if (isset($limit['unit']) && isset($limit['value']) && ($limit['unit'] == 'seconds' || $limit['unit'] == 'second')) { @@ -398,31 +412,31 @@ public function getTimeLimit() { } /** - * Pass messages through to the map class. + * Passes messages through to the map class. * * @param string $message - * The message to record. + * The message to record. * @param int $level - * Optional message severity (defaults to MESSAGE_ERROR). + * (optional) Message severity (defaults to MESSAGE_ERROR). */ public function saveMessage($message, $level = MigrationInterface::MESSAGE_ERROR) { $this->migration->getIdMap()->saveMessage($this->sourceIdValues, $message, $level); } /** - * Queue messages to be later saved through the map class. + * Queues messages to be later saved through the map class. * * @param string $message - * The message to record. + * The message to record. * @param int $level - * Optional message severity (defaults to MESSAGE_ERROR). + * (optional) Message severity (defaults to MESSAGE_ERROR). */ public function queueMessage($message, $level = MigrationInterface::MESSAGE_ERROR) { $this->queuedMessages[] = array('message' => $message, 'level' => $level); } /** - * Save any messages we've queued up to the message table. + * Saves any messages we've queued up to the message table. */ public function saveQueuedMessages() { foreach ($this->queuedMessages as $queued_message) { @@ -432,14 +446,15 @@ public function saveQueuedMessages() { } /** - * Standard top-of-loop stuff, common between rollback and import - check - * for exceptional conditions, and display feedback. + * Checks for exceptional conditions, and display feedback. + * + * Standard top-of-loop stuff, common between rollback and import. */ protected function checkStatus() { if ($this->memoryExceeded()) { return MigrationInterface::RESULT_INCOMPLETE; } - if ($this->timeExceeded()) { + if ($this->maxExecTimeExceeded()) { return MigrationInterface::RESULT_INCOMPLETE; } /* @@ -463,34 +478,36 @@ protected function checkStatus() { } /** - * Test whether we've exceeded the desired memory threshold. If so, output a message. + * Tests whether we've exceeded the desired memory threshold. + * + * If so, output a message. * - * @return boolean - * TRUE if the threshold is exceeded, FALSE if not. + * @return bool + * TRUE if the threshold is exceeded, otherwise FALSE. */ protected function memoryExceeded() { - $usage = memory_get_usage(); + $usage = $this->getMemoryUsage(); $pct_memory = $usage / $this->memoryLimit; - if ($pct_memory > $this->memoryThreshold) { + if (!$threshold = $this->migration->get('memoryThreshold')) { + return FALSE; + } + if ($pct_memory > $threshold) { $this->message->display( - $this->t('Memory usage is !usage (!pct% of limit !limit), resetting statics', + $this->t('Memory usage is !usage (!pct% of limit !limit), reclaiming memory.', array('!pct' => round($pct_memory*100), - '!usage' => format_size($usage), - '!limit' => format_size($this->memoryLimit))), + '!usage' => $this->formatSize($usage), + '!limit' => $this->formatSize($this->memoryLimit))), 'warning'); - // First, try resetting Drupal's static storage - this frequently releases - // plenty of memory to continue - drupal_static_reset(); - $usage = memory_get_usage(); - $pct_memory = $usage/$this->memoryLimit; + $usage = $this->attemptMemoryReclaim(); + $pct_memory = $usage / $this->memoryLimit; // Use a lower threshold - we don't want to be in a situation where we keep // coming back here and trimming a tiny amount - if ($pct_memory > (.90 * $this->memoryThreshold)) { + if ($pct_memory > (0.90 * $threshold)) { $this->message->display( $this->t('Memory usage is now !usage (!pct% of limit !limit), not enough reclaimed, starting new batch', array('!pct' => round($pct_memory*100), - '!usage' => format_size($usage), - '!limit' => format_size($this->memoryLimit))), + '!usage' => $this->formatSize($usage), + '!limit' => $this->formatSize($this->memoryLimit))), 'warning'); return TRUE; } @@ -498,8 +515,8 @@ protected function memoryExceeded() { $this->message->display( $this->t('Memory usage is now !usage (!pct% of limit !limit), reclaimed enough, continuing', array('!pct' => round($pct_memory*100), - '!usage' => format_size($usage), - '!limit' => format_size($this->memoryLimit))), + '!usage' => $this->formatSize($usage), + '!limit' => $this->formatSize($this->memoryLimit))), 'warning'); return FALSE; } @@ -510,36 +527,73 @@ protected function memoryExceeded() { } /** - * Test whether we're approaching the PHP time limit. + * Returns the memory usage so far. * - * @return boolean - * TRUE if the threshold is exceeded, FALSE if not. + * @return int + * The memory usage. */ - protected function timeExceeded() { - if ($this->timeLimit == 0) { - return FALSE; - } - $time_elapsed = time() - REQUEST_TIME; - $pct_time = $time_elapsed / $this->timeLimit; - if ($pct_time > $this->timeThreshold) { - return TRUE; - } - else { - return FALSE; - } + protected function getMemoryUsage() { + return memory_get_usage(); + } + + /** + * Tries to reclaim memory. + * + * @return int + * The memory usage after reclaim. + */ + protected function attemptMemoryReclaim() { + // First, try resetting Drupal's static storage - this frequently releases + // plenty of memory to continue. + drupal_static_reset(); + // @TODO: explore resetting the container. + return memory_get_usage(); } /** - * Takes an Exception object and both saves and displays it, pulling additional - * information on the location triggering the exception. + * Generates a string representation for the given byte count. + * + * @param int $size + * A size in bytes. + * + * @return string + * A translated string representation of the size. + */ + protected function formatSize($size) { + return format_size($size); + } + + /** + * Tests whether we're approaching the PHP maximum execution time limit. + * + * @return bool + * TRUE if the threshold is exceeded, FALSE if not. + */ + protected function maxExecTimeExceeded() { + return $this->maxExecTime && (($this->getTimeElapsed() / $this->maxExecTime) > $this->migration->get('timeThreshold')); + } + + /** + * Returns the time elapsed. + * + * This allows a test to set a fake elapsed time. + */ + protected function getTimeElapsed() { + return time() - REQUEST_TIME; + } + + /** + * Takes an Exception object and both saves and displays it. + * + * Pulls in additional information on the location triggering the exception. * * @param \Exception $exception - * Object representing the exception. - * @param boolean $save - * Whether to save the message in the migration's mapping table. Set to FALSE - * in contexts where this doesn't make sense. + * Object representing the exception. + * @param bool $save + * (optional) Whether to save the message in the migration's mapping table. + * Set to FALSE in contexts where this doesn't make sense. */ - public function handleException($exception, $save = TRUE) { + public function handleException(\Exception $exception, $save = TRUE) { $result = Error::decodeException($exception); $message = $result['!message'] . ' (' . $result['%file'] . ':' . $result['%line'] . ')'; if ($save) { diff --git a/core/modules/migrate/lib/Drupal/migrate/MigrateSkipRowException.php b/core/modules/migrate/lib/Drupal/migrate/MigrateSkipRowException.php new file mode 100644 index 000000000000..3a019533267b --- /dev/null +++ b/core/modules/migrate/lib/Drupal/migrate/MigrateSkipRowException.php @@ -0,0 +1,15 @@ +<?php + +/** + * @file + * Contains \Drupal\migrate\MigrateSkipRowException. + */ + +namespace Drupal\migrate; + +/** + * This exception is thrown when a row should be skipped. + */ +class MigrateSkipRowException extends \Exception { + +} diff --git a/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateIdMapInterface.php b/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateIdMapInterface.php index c1f91c47ff96..4848b9961294 100644 --- a/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateIdMapInterface.php +++ b/core/modules/migrate/lib/Drupal/migrate/Plugin/MigrateIdMapInterface.php @@ -207,7 +207,7 @@ public function lookupSourceID(array $destination_id_values); * @return array * The destination identifier values of the record, or NULL on failure. */ - public function lookupDestinationID(array $source_id_values); + public function lookupDestinationId(array $source_id_values); /** * Removes any persistent storage used by this map. diff --git a/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/id_map/Sql.php b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/id_map/Sql.php index 44df10cef772..7f9cf0eac83f 100644 --- a/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/id_map/Sql.php +++ b/core/modules/migrate/lib/Drupal/migrate/Plugin/migrate/id_map/Sql.php @@ -381,13 +381,13 @@ public function lookupSourceID(array $destination_id) { } $result = $query->execute(); $source_id = $result->fetchAssoc(); - return array_values($source_id); + return array_values($source_id ?: array()); } /** * {@inheritdoc} */ - public function lookupDestinationID(array $source_id) { + public function lookupDestinationId(array $source_id) { $query = $this->getDatabase()->select($this->mapTableName, 'map') ->fields('map', $this->destinationIdFields); foreach ($this->sourceIdFields as $key_name) { @@ -395,7 +395,7 @@ public function lookupDestinationID(array $source_id) { } $result = $query->execute(); $destination_id = $result->fetchAssoc(); - return array_values($destination_id); + return array_values($destination_id ?: array()); } /** @@ -444,9 +444,8 @@ public function saveMessage(array $source_id_values, $message, $level = Migratio $count = 1; foreach ($source_id_values as $id_value) { $fields['sourceid' . $count++] = $id_value; - // If any key value is empty, we can't save - print out and abort. - if (empty($id_value)) { - print($message); + // If any key value is not set, we can't save. + if (!isset($id_value)) { return; } } @@ -579,7 +578,7 @@ public function deleteDestination(array $destination_id) { * {@inheritdoc} */ public function setUpdate(array $source_id) { - if (empty($source_ids)) { + if (empty($source_id)) { throw new MigrateException('No source identifiers provided to update.'); } $query = $this->getDatabase() @@ -695,16 +694,13 @@ public function key() { * from rewind(). */ public function next() { - $this->currentRow = $this->result->fetchObject(); + $this->currentRow = $this->result->fetchAssoc(); $this->currentKey = array(); - if (!is_object($this->currentRow)) { - $this->currentRow = NULL; - } - else { + if ($this->currentRow) { foreach ($this->sourceIdFields as $map_field) { - $this->currentKey[$map_field] = $this->currentRow->$map_field; + $this->currentKey[$map_field] = $this->currentRow[$map_field]; // Leave only destination fields. - unset($this->currentRow->$map_field); + unset($this->currentRow[$map_field]); } } } @@ -717,7 +713,7 @@ public function next() { */ public function valid() { // @todo Check numProcessed against itemlimit. - return !is_null($this->currentRow); + return $this->currentRow !== FALSE; } } diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeConnection.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeConnection.php new file mode 100644 index 000000000000..77247148fdde --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeConnection.php @@ -0,0 +1,160 @@ +<?php + +/** + * @file + * Contains \Drupal\migrate\Tests\FakeConnection. + */ + +namespace Drupal\migrate\Tests; + +use Drupal\Core\Database\Connection; + +/** + * Defines a fake connection for use during testing. + */ +class FakeConnection extends Connection { + + /** + * Table prefix on the database. + * + * @var string + */ + protected $tablePrefix; + + /** + * Connection options for the database. + * + * @var array + */ + protected $connectionOptions; + + /** + * Constructs a FakeConnection. + * + * @param array $database_contents + * The database contents faked as an array. Each key is a table name, each + * value is a list of table rows. + * @param array $connection_options + * (optional) The array of connection options for the database. + * @param string $prefix + * (optional) The table prefix on the database. + */ + public function __construct(array $database_contents, $connection_options = array(), $prefix = '') { + $this->databaseContents = $database_contents; + $this->connectionOptions = $connection_options; + $this->tablePrefix = $prefix; + } + + /** + * {@inheritdoc} + */ + public function getConnectionOptions() { + return $this->connectionOptions; + } + + /** + * {@inheritdoc} + */ + public function tablePrefix($table = 'default') { + return $this->tablePrefix; + } + + /** + * {@inheritdoc} + */ + public function select($table, $alias = NULL, array $options = array()) { + return new FakeSelect($this->databaseContents, $table, $alias); + } + + /** + * {@inheritdoc} + */ + public function insert($table, array $options = array()) { + return new FakeInsert($this->databaseContents, $table); + } + + /** + * {@inheritdoc} + */ + public function merge($table, array $options = array()) { + return new FakeMerge($this->databaseContents, $table); + } + + /** + * {@inheritdoc} + */ + public function update($table, array $options = array()) { + return new FakeUpdate($this->databaseContents, $table); + } + + /** + * {@inheritdoc} + */ + public function truncate($table, array $options = array()) { + return new FakeTruncate($this->databaseContents, $table); + } + + /** + * {@inheritdoc} + */ + public function schema() { + return new FakeDatabaseSchema($this->databaseContents); + } + + /** + * {@inheritdoc} + */ + public function query($query, array $args = array(), $options = array()) { + throw new \Exception('Method not supported'); + } + + /** + * {@inheritdoc} + */ + public function queryRange($query, $from, $count, array $args = array(), array $options = array()) { + throw new \Exception('Method not supported'); + } + + /** + * {@inheritdoc} + */ + public function queryTemporary($query, array $args = array(), array $options = array()) { + throw new \Exception('Method not supported'); + } + + /** + * {@inheritdoc} + */ + public function driver() { + throw new \Exception('Method not supported'); + } + + /** + * {@inheritdoc} + */ + public function databaseType() { + throw new \Exception('Method not supported'); + } + + /** + * {@inheritdoc} + */ + public function createDatabase($database) { + // There is nothing to do. + } + + /** + * {@inheritdoc} + */ + public function mapConditionOperator($operator) { + throw new \Exception('Method not supported'); + } + + /** + * {@inheritdoc} + */ + public function nextId($existing_id = 0) { + throw new \Exception('Method not supported'); + } + +} diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeDatabaseSchema.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeDatabaseSchema.php index bab582fb95af..5c1424dd31e4 100644 --- a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeDatabaseSchema.php +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeDatabaseSchema.php @@ -2,10 +2,9 @@ /** * @file - * Contains \Drupal\migrate\Tests\FakeSelect. + * Contains \Drupal\migrate\Tests\FakeDatabaseSchema. */ - namespace Drupal\migrate\Tests; use Drupal\Core\Database\Schema; @@ -19,11 +18,18 @@ class FakeDatabaseSchema extends Schema { */ public $databaseContents; - public function __construct($database_contents) { + /** + * Constructs a fake database schema. + * + * @param array $database_contents + * The database contents faked as an array. Each key is a table name, each + * value is a list of table rows. + */ + public function __construct(array &$database_contents) { $this->uniqueIdentifier = uniqid('', TRUE); // @todo Maybe we can generate an internal representation. - $this->databaseContents = $database_contents; + $this->databaseContents = &$database_contents; } public function tableExists($table) { diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeInsert.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeInsert.php new file mode 100644 index 000000000000..0074bf3bca30 --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeInsert.php @@ -0,0 +1,71 @@ +<?php + +/** + * @file + * Contains \Drupal\migrate\Tests\FakeInsert. + */ + +namespace Drupal\migrate\Tests; + +use Drupal\Core\Database\Query\Insert; +use Drupal\Core\Database\Query\SelectInterface; + +/** + * Defines FakeInsert for use in database tests. + */ +class FakeInsert extends Insert { + + /** + * The database contents. + * + * @var array + */ + protected $databaseContents; + + /** + * The database table to insert into. + * + * @var string + */ + protected $table; + + /** + * Constructs a fake insert object. + * + * @param array $database_contents + * The database contents faked as an array. Each key is a table name, each + * value is a list of table rows. + * @param string $table + * The table to insert into. + * @param array $options + * (optional) The database options. Not used. + */ + public function __construct(array &$database_contents, $table, array $options = array()) { + $this->databaseContents = &$database_contents; + $this->table = $table; + } + + /** + * {@inheritdoc} + */ + public function useDefaults(array $fields) { + throw new \Exception('This method is not supported'); + } + + /** + * {@inheritdoc} + */ + public function from(SelectInterface $query) { + throw new \Exception('This method is not supported'); + } + + /** + * {@inheritdoc} + */ + public function execute() { + foreach ($this->insertValues as $values) { + $this->databaseContents[$this->table][] = array_combine($this->insertFields, $values); + } + } + +} diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeMerge.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeMerge.php new file mode 100644 index 000000000000..09d9e7725d0e --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeMerge.php @@ -0,0 +1,61 @@ +<?php + +/** + * @file + * Contains \Drupal\migrate\Tests\FakeMerge. + */ + +namespace Drupal\migrate\Tests; + +use Drupal\Core\Database\Query\Condition; +use Drupal\Core\Database\Query\InvalidMergeQueryException; +use Drupal\Core\Database\Query\Merge; + +/** + * Defines FakeMerge for use in database tests. + */ +class FakeMerge extends Merge { + + /** + * Constructs a fake merge object and initializes the conditions. + * + * @param array $database_contents + * The database contents faked as an array. Each key is a table name, each + * value is a list of table rows. + * @param string $table + * The database table to merge into. + */ + function __construct(array &$database_contents, $table) { + $this->databaseContents = &$database_contents; + $this->table = $table; + $this->conditionTable = $table; + $this->condition = new Condition('AND'); + } + + /** + * {@inheritdoc} + */ + public function execute() { + if (!count($this->condition)) { + throw new InvalidMergeQueryException(t('Invalid merge query: no conditions')); + } + $select = new FakeSelect($this->databaseContents, $this->conditionTable, 'c'); + $count = $select + ->condition($this->condition) + ->countQuery() + ->execute() + ->fetchField(); + if ($count) { + $update = new FakeUpdate($this->databaseContents, $this->table); + $update + ->fields($this->updateFields) + ->condition($this->condition) + ->execute(); + return self::STATUS_UPDATE; + } + $insert = new FakeInsert($this->databaseContents, $this->table); + $insert->fields($this->insertFields)->execute(); + return self::STATUS_INSERT; + } + +} diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeSelect.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeSelect.php index 6bf586ed0916..bf49714b130a 100644 --- a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeSelect.php +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeSelect.php @@ -19,7 +19,7 @@ class FakeSelect extends Select { * Contents of the pseudo-database. * * Keys are table names and values are arrays of rows in the table. - * Every row there contains all table fields keyed by field name. + * Every row there contains all table field values keyed by field name. * * @code * array( @@ -51,21 +51,20 @@ class FakeSelect extends Select { /** * Constructs a new FakeSelect. * + * @param array $database_contents + * An array of mocked database content. * @param string $table * The base table name used within fake select. * @param string $alias * The base table alias used within fake select. - * @param array $database_contents - * An array of mocked database content. - * * @param string $conjunction * The operator to use to combine conditions: 'AND' or 'OR'. */ - public function __construct($table, $alias, array $database_contents, $conjunction = 'AND') { + public function __construct(array $database_contents, $table, $alias, $conjunction = 'AND') { + $this->databaseContents = $database_contents; $this->addJoin(NULL, $table, $alias); $this->where = new Condition($conjunction); $this->having = new Condition($conjunction); - $this->databaseContents = $database_contents; } /** diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeStatement.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeStatement.php index fdcf11310f5a..89954da01056 100644 --- a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeStatement.php +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeStatement.php @@ -50,8 +50,11 @@ public function fetchField($index = 0) { * {@inheritdoc} */ public function fetchAssoc() { - $return = $this->current(); - $this->next(); + $return = FALSE; + if ($this->valid()) { + $return = $this->current(); + $this->next(); + } return $return; } @@ -90,6 +93,14 @@ public function fetchAllAssoc($key, $fetch = NULL) { return $return; } + /** + * {@inheritdoc} + */ + public function fetchObject() { + $return = $this->fetchAssoc(); + return $return === FALSE ? FALSE : (object) $return; + } + /** * {@inheritdoc} */ diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeTruncate.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeTruncate.php new file mode 100644 index 000000000000..c1007cd5b644 --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeTruncate.php @@ -0,0 +1,36 @@ +<?php + +/** + * @file + * Contains \Drupal\migrate\Tests\FakeTruncate. + */ + +namespace Drupal\migrate\Tests; + +/** + * Defines FakeTruncate for use in database tests. + */ +class FakeTruncate { + + /** + * Constructs a FakeTruncate object. + * + * @param array $database_contents + * The database contents faked as an array. Each key is a table name, each + * value is a list of table rows. + * @param $table + * The table to truncate. + */ + public function __construct(array &$database_contents, $table) { + $this->databaseContents = &$database_contents; + $this->table = $table; + } + + /** + * Executes the TRUNCATE query. + */ + public function execute() { + $this->databaseContents[$this->table] = array(); + } + +} diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/FakeUpdate.php b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeUpdate.php new file mode 100644 index 000000000000..8bf440545cf1 --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/FakeUpdate.php @@ -0,0 +1,88 @@ +<?php + +/** + * @file + * Contains \Drupal\migrate\Tests\FakeUpdate. + */ + +namespace Drupal\migrate\Tests; + +use Drupal\Core\Database\Query\Condition; +use Drupal\Core\Database\Query\SelectInterface; +use Drupal\Core\Database\Query\Update; + +/** + * Defines FakeUpdate for use in database tests. + */ +class FakeUpdate extends Update { + + /** + * The database table to update. + * + * @var string + */ + protected $table; + + /** + * The database contents. + * + * @var array + */ + protected $databaseContents; + + /** + * Constructs a FakeUpdate object and initializes the condition. + * + * @param array $database_contents + * The database contents faked as an array. Each key is a table name, each + * value is a list of table rows. + * @param string $table + * The table to update. + */ + public function __construct(array &$database_contents, $table) { + $this->databaseContents = &$database_contents; + $this->table = $table; + $this->condition = new Condition('AND'); + } + + /** + * {@inheritdoc} + */ + public function execute() { + $affected = 0; + if (isset($this->databaseContents[$this->table])) { + $fields = $this->fields; + $condition = $this->condition; + array_walk($this->databaseContents[$this->table], function (&$row_array) use ($fields, $condition, &$affected) { + $row = new DatabaseRow($row_array); + if (ConditionResolver::matchGroup($row, $condition)) { + $row_array = $fields + $row_array; + $affected++; + } + }); + } + return $affected; + } + + /** + * {@inheritdoc} + */ + public function exists(SelectInterface $select) { + throw new \Exception(sprintf('Method "%s" is not supported', __METHOD__)); + } + + /** + * {@inheritdoc} + */ + public function where($snippet, $args = array()) { + throw new \Exception(sprintf('Method "%s" is not supported', __METHOD__)); + } + + /** + * {@inheritdoc} + */ + public function expression($field, $expression, array $arguments = NULL) { + throw new \Exception(sprintf('Method "%s" is not supported', __METHOD__)); + } + +} diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecutableTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecutableTest.php index 80beaacff62f..cd54fd9ae186 100644 --- a/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecutableTest.php +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecutableTest.php @@ -7,9 +7,10 @@ namespace Drupal\migrate\Tests; -use Drupal\Core\StringTranslation\TranslationInterface; use Drupal\migrate\Entity\MigrationInterface; -use Drupal\migrate\MigrateExecutable; +use Drupal\migrate\Plugin\MigrateIdMapInterface; +use Drupal\migrate\MigrateException; +use Drupal\migrate\Row; /** * Tests the migrate executable. @@ -38,7 +39,7 @@ class MigrateExecutableTest extends MigrateTestCase { /** * The tested migrate executable. * - * @var \Drupal\migrate\MigrateExecutable + * @var \Drupal\migrate\Tests\TestMigrateExecutable */ protected $executable; @@ -46,7 +47,8 @@ class MigrateExecutableTest extends MigrateTestCase { protected $migrationConfiguration = array( 'id' => 'test', - 'limit' => array('units' => 'seconds', 'value' => 5), + 'limit' => array('unit' => 'second', 'value' => 1), + 'timeThreshold' => 0.9, ); /** @@ -60,14 +62,13 @@ public static function getInfo() { ); } + /** + * {@inheritdoc} + */ protected function setUp() { + parent::setUp(); $this->migration = $this->getMigration(); $this->message = $this->getMock('Drupal\migrate\MigrateMessageInterface'); - $id_map = $this->getMock('Drupal\migrate\Plugin\MigrateIdMapInterface'); - - $this->migration->expects($this->any()) - ->method('getIdMap') - ->will($this->returnValue($id_map)); $this->executable = new TestMigrateExecutable($this->migration, $this->message); $this->executable->setTranslationManager($this->getStringTranslationStub()); @@ -80,10 +81,8 @@ public function testImportWithFailingRewind() { $iterator = $this->getMock('\Iterator'); $exception_message = $this->getRandomGenerator()->string(); $iterator->expects($this->once()) - ->method('valid') - ->will($this->returnCallback(function() use ($exception_message) { - throw new \Exception($exception_message); - })); + ->method('rewind') + ->will($this->throwException(new \Exception($exception_message))); $source = $this->getMock('Drupal\migrate\Plugin\MigrateSourceInterface'); $source->expects($this->any()) ->method('getIterator') @@ -102,11 +101,367 @@ public function testImportWithFailingRewind() { $this->assertEquals(MigrationInterface::RESULT_FAILED, $result); } -} + /** + * Tests the import method with a valid row. + */ + public function testImportWithValidRow() { + $source = $this->getMockSource(); + + $row = $this->getMockBuilder('Drupal\migrate\Row') + ->disableOriginalConstructor() + ->getMock(); + + $source->expects($this->once()) + ->method('current') + ->will($this->returnValue($row)); + + $this->executable->setSource($source); + + $this->migration->expects($this->once()) + ->method('getProcessPlugins') + ->will($this->returnValue(array())); + + $destination = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface'); + $destination->expects($this->once()) + ->method('import') + ->with($row) + ->will($this->returnValue(array('id' => 'test'))); + + $this->migration->expects($this->once()) + ->method('getDestinationPlugin') + ->will($this->returnValue($destination)); + + $this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import()); + + $this->assertSame(1, $this->executable->getSuccessesSinceFeedback()); + $this->assertSame(1, $this->executable->getTotalSuccesses()); + $this->assertSame(1, $this->executable->getTotalProcessed()); + $this->assertSame(1, $this->executable->getProcessedSinceFeedback()); + } + + /** + * Tests the import method with a valid row. + */ + public function testImportWithValidRowNoDestinationValues() { + $source = $this->getMockSource(); + + $row = $this->getMockBuilder('Drupal\migrate\Row') + ->disableOriginalConstructor() + ->getMock(); + + $row->expects($this->once()) + ->method('getSourceIdValues') + ->will($this->returnValue(array('id' => 'test'))); + + $source->expects($this->once()) + ->method('current') + ->will($this->returnValue($row)); + + $this->executable->setSource($source); + + $this->migration->expects($this->once()) + ->method('getProcessPlugins') + ->will($this->returnValue(array())); + + $destination = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface'); + $destination->expects($this->once()) + ->method('import') + ->with($row) + ->will($this->returnValue(array())); + + $this->migration->expects($this->once()) + ->method('getDestinationPlugin') + ->will($this->returnValue($destination)); + + $this->idMap->expects($this->once()) + ->method('delete') + ->with(array('id' => 'test'), TRUE); + + $this->idMap->expects($this->once()) + ->method('saveIdMapping') + ->with($row, array(), MigrateIdMapInterface::STATUS_FAILED, NULL); + + $this->idMap->expects($this->once()) + ->method('messageCount') + ->will($this->returnValue(0)); + + $this->idMap->expects($this->once()) + ->method('saveMessage'); + + $this->message->expects($this->once()) + ->method('display') + ->with('New object was not saved, no error provided'); + + $this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import()); + } + + /** + * Tests the import method with a MigrateException being thrown. + */ + public function testImportWithValidRowWithMigrateException() { + $exception_message = $this->getRandomGenerator()->string(); + $source = $this->getMockSource(); + + $row = $this->getMockBuilder('Drupal\migrate\Row') + ->disableOriginalConstructor() + ->getMock(); + + $row->expects($this->once()) + ->method('getSourceIdValues') + ->will($this->returnValue(array('id' => 'test'))); + + $source->expects($this->once()) + ->method('current') + ->will($this->returnValue($row)); + + $this->executable->setSource($source); + + $this->migration->expects($this->once()) + ->method('getProcessPlugins') + ->will($this->returnValue(array())); + + $destination = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface'); + $destination->expects($this->once()) + ->method('import') + ->with($row) + ->will($this->throwException(new MigrateException($exception_message))); + + $this->migration->expects($this->once()) + ->method('getDestinationPlugin') + ->will($this->returnValue($destination)); + + $this->idMap->expects($this->once()) + ->method('saveIdMapping') + ->with($row, array(), MigrateIdMapInterface::STATUS_FAILED, NULL); + + $this->idMap->expects($this->once()) + ->method('saveMessage'); + + $this->message->expects($this->once()) + ->method('display') + ->with($exception_message); + + $this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import()); + } + + /** + * Tests the import method with a regular Exception being thrown. + */ + public function testImportWithValidRowWithException() { + $exception_message = $this->getRandomGenerator()->string(); + $source = $this->getMockSource(); + + $row = $this->getMockBuilder('Drupal\migrate\Row') + ->disableOriginalConstructor() + ->getMock(); + + $row->expects($this->once()) + ->method('getSourceIdValues') + ->will($this->returnValue(array('id' => 'test'))); + + $source->expects($this->once()) + ->method('current') + ->will($this->returnValue($row)); + + $this->executable->setSource($source); + + $this->migration->expects($this->once()) + ->method('getProcessPlugins') + ->will($this->returnValue(array())); + + $destination = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface'); + $destination->expects($this->once()) + ->method('import') + ->with($row) + ->will($this->throwException(new \Exception($exception_message))); + + $this->migration->expects($this->once()) + ->method('getDestinationPlugin') + ->will($this->returnValue($destination)); + + $this->idMap->expects($this->once()) + ->method('saveIdMapping') + ->with($row, array(), MigrateIdMapInterface::STATUS_FAILED, NULL); + + $this->idMap->expects($this->once()) + ->method('saveMessage'); + + $this->message->expects($this->once()) + ->method('display') + ->with($exception_message); + + $this->assertSame(MigrationInterface::RESULT_COMPLETED, $this->executable->import()); + } + + /** + * Tests time limit option method. + */ + public function testTimeOptionExceeded() { + // Assert time limit of one second (test configuration default) is exceeded. + $this->executable->setTimeElapsed(1); + $this->assertTrue($this->executable->timeOptionExceeded()); + // Assert time limit not exceeded. + $this->migration->set('limit', array('unit' => 'seconds', 'value' => (REQUEST_TIME - 3600))); + $this->assertFalse($this->executable->timeOptionExceeded()); + // Assert no time limit. + $this->migration->set('limit', array()); + $this->assertFalse($this->executable->timeOptionExceeded()); + } + + /** + * Tests get time limit method. + */ + public function testGetTimeLimit() { + // Assert time limit has a unit of one second (test configuration default). + $limit = $this->migration->get('limit'); + $this->assertArrayHasKey('unit', $limit); + $this->assertSame('second', $limit['unit']); + $this->assertSame($limit['value'], $this->executable->getTimeLimit()); + // Assert time limit has a unit of multiple seconds. + $this->migration->set('limit', array('unit' => 'seconds', 'value' => 30)); + $limit = $this->migration->get('limit'); + $this->assertArrayHasKey('unit', $limit); + $this->assertSame('seconds', $limit['unit']); + $this->assertSame($limit['value'], $this->executable->getTimeLimit()); + // Assert no time limit. + $this->migration->set('limit', array()); + $limit = $this->migration->get('limit'); + $this->assertArrayNotHasKey('unit', $limit); + $this->assertArrayNotHasKey('value', $limit); + $this->assertNull($this->executable->getTimeLimit()); + } + + /** + * Tests saving of queued messages. + */ + public function testSaveQueuedMessages() { + // Assert no queued messages before save. + $this->assertAttributeEquals(array(), 'queuedMessages', $this->executable); + // Set required source_id_values for MigrateIdMapInterface::saveMessage(). + $expected_messages[] = array('message' => 'message 1', 'level' => MigrationInterface::MESSAGE_ERROR); + $expected_messages[] = array('message' => 'message 2', 'level' => MigrationInterface::MESSAGE_WARNING); + $expected_messages[] = array('message' => 'message 3', 'level' => MigrationInterface::MESSAGE_INFORMATIONAL); + foreach ($expected_messages as $queued_message) { + $this->executable->queueMessage($queued_message['message'], $queued_message['level']); + } + $this->executable->setSourceIdValues(array()); + $this->assertAttributeEquals($expected_messages, 'queuedMessages', $this->executable); + // No asserts of saved messages since coverage exists + // in MigrateSqlIdMapTest::saveMessage(). + $this->executable->saveQueuedMessages(); + // Assert no queued messages after save. + $this->assertAttributeEquals(array(), 'queuedMessages', $this->executable); + } + + /** + * Tests the queuing of messages. + */ + public function testQueueMessage() { + // Assert no queued messages. + $expected_messages = array(); + $this->assertAttributeEquals(array(), 'queuedMessages', $this->executable); + // Assert a single (default level) queued message. + $expected_messages[] = array( + 'message' => 'message 1', + 'level' => MigrationInterface::MESSAGE_ERROR, + ); + $this->executable->queueMessage('message 1'); + $this->assertAttributeEquals($expected_messages, 'queuedMessages', $this->executable); + // Assert multiple queued messages. + $expected_messages[] = array( + 'message' => 'message 2', + 'level' => MigrationInterface::MESSAGE_WARNING, + ); + $this->executable->queueMessage('message 2', MigrationInterface::MESSAGE_WARNING); + $this->assertAttributeEquals($expected_messages, 'queuedMessages', $this->executable); + $expected_messages[] = array( + 'message' => 'message 3', + 'level' => MigrationInterface::MESSAGE_INFORMATIONAL, + ); + $this->executable->queueMessage('message 3', MigrationInterface::MESSAGE_INFORMATIONAL); + $this->assertAttributeEquals($expected_messages, 'queuedMessages', $this->executable); + } -class TestMigrateExecutable extends MigrateExecutable { + /** + * Tests maximum execution time (max_execution_time) of an import. + */ + public function testMaxExecTimeExceeded() { + // Assert no max_execution_time value. + $this->executable->setMaxExecTime(0); + $this->assertFalse($this->executable->maxExecTimeExceeded()); + // Assert default max_execution_time value does not exceed. + $this->executable->setMaxExecTime(30); + $this->assertFalse($this->executable->maxExecTimeExceeded()); + // Assert max_execution_time value is exceeded. + $this->executable->setMaxExecTime(1); + $this->executable->setTimeElapsed(2); + $this->assertTrue($this->executable->maxExecTimeExceeded()); + } - public function setTranslationManager(TranslationInterface $translation_manager) { - $this->translationManager = $translation_manager; + /** + * Tests the processRow method. + */ + public function testProcessRow() { + $expected = array( + 'test' => 'test destination', + 'test1' => 'test1 destination' + ); + foreach ($expected as $key => $value) { + $plugins[$key][0] = $this->getMock('Drupal\migrate\Plugin\MigrateProcessInterface'); + $plugins[$key][0]->expects($this->once()) + ->method('getPluginDefinition') + ->will($this->returnValue(array())); + $plugins[$key][0]->expects($this->once()) + ->method('transform') + ->will($this->returnValue($value)); + } + $this->migration->expects($this->once()) + ->method('getProcessPlugins') + ->with(NULL) + ->will($this->returnValue($plugins)); + $row = new Row(array(), array()); + $this->executable->processRow($row); + foreach ($expected as $key => $value) { + $this->assertSame($row->getDestinationProperty($key), $value); + } + $this->assertSame(count($row->getDestination()), count($expected)); + } + + /** + * Tests the processRow method with an empty pipeline. + */ + public function testProcessRowEmptyPipeline() { + $this->migration->expects($this->once()) + ->method('getProcessPlugins') + ->with(NULL) + ->will($this->returnValue(array('test' => array()))); + $row = new Row(array(), array()); + $this->executable->processRow($row); + $this->assertSame($row->getDestination(), array()); } + + /** + * Returns a mock migration source instance. + * + * @return \Drupal\migrate\Source|\PHPUnit_Framework_MockObject_MockObject + */ + protected function getMockSource() { + $iterator = $this->getMock('\Iterator'); + + $source = $this->getMockBuilder('Drupal\migrate\Source') + ->disableOriginalConstructor() + ->getMock(); + $source->expects($this->any()) + ->method('getIterator') + ->will($this->returnValue($iterator)); + $source->expects($this->once()) + ->method('rewind') + ->will($this->returnValue(TRUE)); + $source->expects($this->any()) + ->method('valid') + ->will($this->onConsecutiveCalls(TRUE, FALSE)); + + return $source; + } + } diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecuteableMemoryExceededTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecuteableMemoryExceededTest.php new file mode 100644 index 000000000000..d69873fae0f4 --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateExecuteableMemoryExceededTest.php @@ -0,0 +1,139 @@ +<?php + +/** + * @file + * Contains \Drupal\migrate\Tests\MigrateExecuteableMemoryExceeded. + */ + +namespace Drupal\migrate\Tests; + +/** + * Tests the \Drupal\migrate\MigrateExecutable::memoryExceeded() method. + * + * @group Drupal + * @group migrate + */ +class MigrateExecuteableMemoryExceededTest extends MigrateTestCase { + + /** + * The mocked migration entity. + * + * @var \Drupal\migrate\Entity\MigrationInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $migration; + + /** + * The mocked migrate message. + * + * @var \Drupal\migrate\MigrateMessageInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $message; + + /** + * The tested migrate executable. + * + * @var \Drupal\migrate\Tests\TestMigrateExecutable + */ + protected $executable; + + /** + * Whether the map is joinable, initialized to FALSE. + * + * @var bool + */ + protected $mapJoinable = FALSE; + + /** + * The migration configuration, initialized to set the ID to test. + * + * @var array + */ + protected $migrationConfiguration = array( + 'id' => 'test', + ); + + /** + * php.init memory_limit value. + */ + protected $memoryLimit = 10000000; + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'Migrate executable memory exceeded', + 'description' => 'Tests the migrate executable memoryExceeded method.', + 'group' => 'Migrate', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->migration = $this->getMigration(); + $this->message = $this->getMock('Drupal\migrate\MigrateMessageInterface'); + + $this->executable = new TestMigrateExecutable($this->migration, $this->message); + $this->executable->setTranslationManager($this->getStringTranslationStub()); + } + + /** + * Runs the actual test. + * + * @param string $message + * The second message to assert. + * @param bool $memory_exceeded + * Whether to test the memory exceeded case. + * @param int $memory_usage_first + * (optional) The first memory usage value. + * @param int $memory_usage_second + * (optional) The fake amount of memory usage reported after memory reclaim. + * @param int $memory_limit + * (optional) The memory limit. + */ + protected function runMemoryExceededTest($message, $memory_exceeded, $memory_usage_first = NULL, $memory_usage_second = NULL, $memory_limit = NULL) { + $this->executable->setMemoryLimit($memory_limit ?: $this->memoryLimit); + $this->executable->setMemoryUsage($memory_usage_first ?: $this->memoryLimit, $memory_usage_second ?: $this->memoryLimit); + $this->migration->set('memoryThreshold', 0.85); + if ($message) { + $this->executable->message->expects($this->at(0)) + ->method('display') + ->with($this->stringContains('reclaiming memory')); + $this->executable->message->expects($this->at(1)) + ->method('display') + ->with($this->stringContains($message)); + } + else { + $this->executable->message->expects($this->never()) + ->method($this->anything()); + } + $result = $this->executable->memoryExceeded(); + $this->assertEquals($memory_exceeded, $result); + } + + /** + * Tests memoryExceeded method when a new batch is needed. + */ + public function testMemoryExceededNewBatch() { + // First case try reset and then start new batch. + $this->runMemoryExceededTest('starting new batch', TRUE); + } + + /** + * Tests memoryExceeded method when enough is cleared. + */ + public function testMemoryExceededClearedEnough() { + $this->runMemoryExceededTest('reclaimed enough', FALSE, $this->memoryLimit, $this->memoryLimit * 0.75); + } + + /** + * Tests memoryExceeded when memory usage is not exceeded. + */ + public function testMemoryNotExceeded() { + $this->runMemoryExceededTest('', FALSE, floor($this->memoryLimit * 0.85) - 1); + } + +} diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapEnsureTablesTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapEnsureTablesTest.php new file mode 100644 index 000000000000..28393febd89a --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapEnsureTablesTest.php @@ -0,0 +1,205 @@ +<?php + +/** + * @file + * Contains \Drupal\migrate\Tests\MigrateSqlIdMapEnsureTablesTest. + */ + +namespace Drupal\migrate\Tests; + +use Drupal\migrate\Plugin\MigrateIdMapInterface; + +/** + * Tests the \Drupal\migrate\Plugin\migrate\id_map\Sql::ensureTables() method. + * + * @group Drupal + * @group migrate + */ +class MigrateSqlIdMapEnsureTablesTest extends MigrateTestCase { + + /** + * Whether the map is joinable, initialized to FALSE. + * + * @var bool + */ + protected $mapJoinable = FALSE; + + /** + * The migration configuration, initialized to set the ID and destination IDs. + * + * @var array + */ + protected $migrationConfiguration = array( + 'id' => 'sql_idmap_test', + 'sourceIds' => array( + 'source_id_property' => array( + 'type' => 'int', + ), + ), + 'destinationIds' => array( + 'destination_id_property' => array( + 'type' => 'varchar', + 'length' => 255, + ), + ), + ); + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'Sql::ensureTables()', + 'description' => 'Tests the SQL ID map plugin ensureTables() method.', + 'group' => 'Migrate', + ); + } + + /** + * Tests the ensureTables method when the tables do not exist. + */ + public function testEnsureTablesNotExist() { + $fields['source_row_status'] = array( + 'type' => 'int', + 'size' => 'tiny', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => MigrateIdMapInterface::STATUS_IMPORTED, + 'description' => 'Indicates current status of the source row', + ); + $fields['rollback_action'] = array( + 'type' => 'int', + 'size' => 'tiny', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => MigrateIdMapInterface::ROLLBACK_DELETE, + 'description' => 'Flag indicating what to do for this item on rollback', + ); + $fields['last_imported'] = array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'UNIX timestamp of the last time this row was imported', + ); + $fields['hash'] = array( + 'type' => 'varchar', + 'length' => '32', + 'not null' => FALSE, + 'description' => 'Hash of source row data, for detecting changes', + ); + $fields['sourceid1'] = $this->migrationConfiguration['sourceIds']['source_id_property']; + $fields['destid1'] = $this->migrationConfiguration['destinationIds']['destination_id_property']; + $fields['destid1']['not null'] = FALSE; + $map_table_schema = array( + 'description' => 'Mappings from source identifier value(s) to destination identifier value(s).', + 'fields' => $fields, + 'primary key' => array('sourceid1'), + ); + $schema = $this->getMockBuilder('Drupal\Core\Database\Schema') + ->disableOriginalConstructor() + ->getMock(); + $schema->expects($this->at(0)) + ->method('tableExists') + ->with('migrate_map_sql_idmap_test') + ->will($this->returnValue(FALSE)); + $schema->expects($this->at(1)) + ->method('createTable') + ->with('migrate_map_sql_idmap_test', $map_table_schema); + // Now do the message table. + $fields = array(); + $fields['msgid'] = array( + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ); + $fields['sourceid1'] = $this->migrationConfiguration['sourceIds']['source_id_property']; + $fields['level'] = array( + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 1, + ); + $fields['message'] = array( + 'type' => 'text', + 'size' => 'medium', + 'not null' => TRUE, + ); + $table_schema = array( + 'description' => 'Messages generated during a migration process', + 'fields' => $fields, + 'primary key' => array('msgid'), + ); + $table_schema['indexes']['sourcekey'] = array('sourceid1'); + + $schema->expects($this->at(2)) + ->method('createTable') + ->with('migrate_message_sql_idmap_test', $table_schema); + $schema->expects($this->exactly(3)) + ->method($this->anything()); + $this->runEnsureTablesTest($schema); + } + + /** + * Tests the ensureTables method when the tables exist. + */ + public function testEnsureTablesExist() { + $schema = $this->getMockBuilder('Drupal\Core\Database\Schema') + ->disableOriginalConstructor() + ->getMock(); + $schema->expects($this->at(0)) + ->method('tableExists') + ->with('migrate_map_sql_idmap_test') + ->will($this->returnValue(TRUE)); + $schema->expects($this->at(1)) + ->method('fieldExists') + ->with('migrate_map_sql_idmap_test', 'rollback_action') + ->will($this->returnValue(FALSE)); + $field_schema = array( + 'type' => 'int', + 'size' => 'tiny', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'description' => 'Flag indicating what to do for this item on rollback', + ); + $schema->expects($this->at(2)) + ->method('addField') + ->with('migrate_map_sql_idmap_test', 'rollback_action', $field_schema); + $schema->expects($this->at(3)) + ->method('fieldExists') + ->with('migrate_map_sql_idmap_test', 'hash') + ->will($this->returnValue(FALSE)); + $field_schema = array( + 'type' => 'varchar', + 'length' => '32', + 'not null' => FALSE, + 'description' => 'Hash of source row data, for detecting changes', + ); + $schema->expects($this->at(4)) + ->method('addField') + ->with('migrate_map_sql_idmap_test', 'hash', $field_schema); + $schema->expects($this->exactly(5)) + ->method($this->anything()); + $this->runEnsureTablesTest($schema); + } + + /** + * Actually run the test. + * + * @param array $schema + * The mock schema object with expectations set. The Sql constructor calls + * ensureTables() which in turn calls this object and the expectations on + * it are the actual test and there are no additional asserts added. + */ + protected function runEnsureTablesTest($schema) { + $database = $this->getMockBuilder('Drupal\Core\Database\Connection') + ->disableOriginalConstructor() + ->getMock(); + $database->expects($this->any()) + ->method('schema') + ->will($this->returnValue($schema)); + new TestSqlIdMap($database, array(), 'sql', array(), $this->getMigration()); + } + +} diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapTest.php new file mode 100644 index 000000000000..9bce1722b852 --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateSqlIdMapTest.php @@ -0,0 +1,741 @@ +<?php + +/** + * @file + * Contains \Drupal\migrate\Tests\MigrateSqlIdMapTestCase. + */ + +namespace Drupal\migrate\Tests; + +use Drupal\migrate\Entity\MigrationInterface; +use Drupal\migrate\MigrateException; +use Drupal\migrate\Plugin\MigrateIdMapInterface; +use Drupal\migrate\Row; + +/** + * Tests the SQL based ID map implementation. + * + * @group Drupal + * @group migrate + */ +class MigrateSqlIdMapTest extends MigrateTestCase { + + /** + * Whether the map is joinable, initialized to FALSE. + * + * @var bool + */ + protected $mapJoinable = FALSE; + + /** + * The migration configuration, initialized to set the ID and destination IDs. + * + * @var array + */ + protected $migrationConfiguration = array( + 'id' => 'sql_idmap_test', + 'sourceIds' => array( + 'source_id_property' => array(), + ), + 'destinationIds' => array( + 'destination_id_property' => array(), + ), + ); + + /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'SQL ID map plugin', + 'description' => 'Tests the SQL ID map plugin.', + 'group' => 'Migrate', + ); + } + + /** + * Creates a test SQL ID map plugin. + * + * @param array $database_contents + * (optional) An array keyed by table names. Value are list of rows where + * each row is an associative array of field => value. + * @param array $connection_options + * (optional) An array of database connection options. + * @param string $prefix + * (optional) The database prefix. + * + * @return \Drupal\migrate\Tests\TestSqlIdMap + * A SQL ID map plugin test instance. + */ + protected function getIdMap($database_contents = array(), $connection_options = array(), $prefix = '') { + $migration = $this->getMigration(); + $this->database = $this->getDatabase($database_contents, $connection_options, $prefix); + $id_map = new TestSqlIdMap($this->database, array(), 'sql', array(), $migration); + $migration->expects($this->any()) + ->method('getIdMap') + ->will($this->returnValue($id_map)); + return $id_map; + } + + /** + * Sets defaults for SQL ID map plugin tests. + */ + protected function idMapDefaults() { + return array( + 'source_row_status' => MigrateIdMapInterface::STATUS_IMPORTED, + 'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE, + 'hash' => '', + ); + } + + /** + * Tests the ID mapping method. + * + * Create two ID mappings and update the second to verify that: + * - saving new to empty tables work. + * - saving new to nonempty tables work. + * - updating work. + */ + public function testSaveIdMapping() { + $source = array( + 'source_id_property' => 'source_value', + ); + $row = new Row($source, array('source_id_property' => array())); + $id_map = $this->getIdMap(); + $id_map->saveIdMapping($row, array('destination_id_property' => 2)); + $expected_result = array( + array( + 'sourceid1' => 'source_value', + 'destid1' => 2, + ) + $this->idMapDefaults(), + ); + $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_result); + $source = array( + 'source_id_property' => 'source_value_1', + ); + $row = new Row($source, array('source_id_property' => array())); + $id_map->saveIdMapping($row, array('destination_id_property' => 3)); + $expected_result[] = array( + 'sourceid1' => 'source_value_1', + 'destid1' => 3, + ) + $this->idMapDefaults(); + $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_result); + $id_map->saveIdMapping($row, array('destination_id_property' => 4)); + $expected_result[1]['destid1'] = 4; + $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_result); + } + + /** + * Tests the SQL ID map set message method. + */ + public function testSetMessage() { + $message = $this->getMock('Drupal\migrate\MigrateMessageInterface'); + $id_map = $this->getIdMap(); + $id_map->setMessage($message); + $this->assertAttributeEquals($message, 'message', $id_map); + } + + /** + * Tests the clear messages method. + */ + public function testClearMessages() { + $message = 'Hello world.'; + $expected_results = array(0, 1, 2, 3); + $id_map = $this->getIdMap(); + + // Insert 4 message for later delete. + foreach ($expected_results as $key => $expected_result) { + $id_map->saveMessage(array($key), $message); + } + + // Truncate and check that 4 messages were deleted. + $this->assertEquals($id_map->messageCount(), 4); + $id_map->clearMessages(); + $count = $id_map->messageCount(); + $this->assertEquals($count, 0); + } + + /** + * Tests the getRowsNeedingUpdate method for rows that need an update. + */ + public function testGetRowsNeedingUpdate() { + $id_map = $this->getIdMap(); + $row_statuses = array( + MigrateIdMapInterface::STATUS_IMPORTED, + MigrateIdMapInterface::STATUS_NEEDS_UPDATE, + MigrateIdMapInterface::STATUS_IGNORED, + MigrateIdMapInterface::STATUS_FAILED, + ); + // Create a mapping row for each STATUS constant. + foreach ($row_statuses as $status) { + $source = array('source_id_property' => 'source_value_' . $status); + $row = new Row($source, array('source_id_property' => array())); + $destination = array('destination_id_property' => 'destination_value_' . $status); + $id_map->saveIdMapping($row, $destination, $status); + $expected_results[] = array( + 'sourceid1' => 'source_value_' . $status, + 'destid1' => 'destination_value_' . $status, + 'source_row_status' => $status, + 'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE, + 'hash' => '', + ); + // Assert zero rows need an update. + if ($status == MigrateIdMapInterface::STATUS_IMPORTED) { + $rows_needing_update = $id_map->getRowsNeedingUpdate(1); + $this->assertCount(0, $rows_needing_update); + } + } + // Assert that test values exist. + $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_results); + + // Assert a single row needs an update. + $row_needing_update = $id_map->getRowsNeedingUpdate(1); + $this->assertCount(1, $row_needing_update); + + // Assert the row matches its original source. + $source_id = $expected_results[MigrateIdMapInterface::STATUS_NEEDS_UPDATE]['sourceid1']; + $test_row = $id_map->getRowBySource(array($source_id)); + $this->assertSame($test_row, $row_needing_update[0]); + + // Add additional row that needs an update. + $source = array('source_id_property' => 'source_value_multiple'); + $row = new Row($source, array('source_id_property' => array())); + $destination = array('destination_id_property' => 'destination_value_multiple'); + $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_NEEDS_UPDATE); + + // Assert multiple rows need an update. + $rows_needing_update = $id_map->getRowsNeedingUpdate(2); + $this->assertCount(2, $rows_needing_update); + } + + /** + * Tests the SQL ID map message count method by counting and saving messages. + */ + public function testMessageCount() { + $message = 'Hello world.'; + $expected_results = array(0, 1, 2, 3); + $id_map = $this->getIdMap(); + + // Test count message multiple times starting from 0. + foreach ($expected_results as $key => $expected_result) { + $count = $id_map->messageCount(); + $this->assertEquals($expected_result, $count); + $id_map->saveMessage(array($key), $message); + } + } + + /** + * Tests the SQL ID map save message method. + */ + public function testMessageSave() { + $message = 'Hello world.'; + $expected_results = array( + 1 => array('message' => $message, 'level' => MigrationInterface::MESSAGE_ERROR), + 2 => array('message' => $message, 'level' => MigrationInterface::MESSAGE_WARNING), + 3 => array('message' => $message, 'level' => MigrationInterface::MESSAGE_NOTICE), + 4 => array('message' => $message, 'level' => MigrationInterface::MESSAGE_INFORMATIONAL), + ); + $id_map = $this->getIdMap(); + + foreach ($expected_results as $key => $expected_result) { + $id_map->saveMessage(array($key), $message, $expected_result['level']); + $message_row = $this->database->select($id_map->getMessageTableName(), 'message') + ->fields('message') + ->condition('level',$expected_result['level']) + ->condition('message',$expected_result['message']) + ->execute() + ->fetchAssoc(); + $this->assertEquals($expected_result['message'], $message_row['message'], 'Message from database was read.'); + } + + // Insert with default level. + $message_default = 'Hello world default.'; + $id_map->saveMessage(array(5), $message_default); + $message_row = $this->database->select($id_map->getMessageTableName(), 'message') + ->fields('message') + ->condition('level', MigrationInterface::MESSAGE_ERROR) + ->condition('message', $message_default) + ->execute() + ->fetchAssoc(); + $this->assertEquals($message_default, $message_row['message'], 'Message from database was read.'); + } + + /** + * Tests the getRowBySource method. + */ + public function testGetRowBySource() { + $row = array( + 'sourceid1' => 'source_id_value_1', + 'sourceid2' => 'source_id_value_2', + 'destid1' => 'destination_id_value_1', + ) + $this->idMapDefaults(); + $database_contents['migrate_map_sql_idmap_test'][] = $row; + $row = array( + 'sourceid1' => 'source_id_value_3', + 'sourceid2' => 'source_id_value_4', + 'destid1' => 'destination_id_value_2', + ) + $this->idMapDefaults(); + $database_contents['migrate_map_sql_idmap_test'][] = $row; + $source_id_values = array($row['sourceid1'], $row['sourceid2']); + $id_map = $this->getIdMap($database_contents); + $result_row = $id_map->getRowBySource($source_id_values); + $this->assertSame($row, $result_row); + $source_id_values = array('missing_value_1', 'missing_value_2'); + $result_row = $id_map->getRowBySource($source_id_values); + $this->assertFalse($result_row); + } + + /** + * Tests the destination ID lookup method. + * + * Scenarios to test (for both hits and misses) are: + * - Single-value source ID to single-value destination ID. + * - Multi-value source ID to multi-value destination ID. + * - Single-value source ID to multi-value destination ID. + * - Multi-value source ID to single-value destination ID. + */ + public function testLookupDestinationIdMapping() { + $this->performLookupDestinationIdTest(1, 1); + $this->performLookupDestinationIdTest(2, 2); + $this->performLookupDestinationIdTest(1, 2); + $this->performLookupDestinationIdTest(2, 1); + } + + /** + * Performs destination ID test on source and destination fields. + * + * @param int $num_source_fields + * Number of source fields to test. + * @param int $num_destination_fields + * Number of destination fields to test. + */ + protected function performLookupDestinationIdTest($num_source_fields, $num_destination_fields) { + // Adjust the migration configuration according to the number of source and + // destination fields. + $this->migrationConfiguration['sourceIds'] = array(); + $this->migrationConfiguration['destinationIds'] = array(); + $source_id_values = array(); + $nonexistent_id_values = array(); + $row = $this->idMapDefaults(); + for ($i = 1; $i <= $num_source_fields; $i++) { + $row["sourceid$i"] = "source_id_value_$i"; + $source_id_values[] = "source_id_value_$i"; + $nonexistent_id_values[] = "nonexistent_source_id_value_$i"; + $this->migrationConfiguration['sourceIds']["source_id_property_$i"] = array(); + } + $expected_result = array(); + for ($i = 1; $i <= $num_destination_fields; $i++) { + $row["destid$i"] = "destination_id_value_$i"; + $expected_result[] = "destination_id_value_$i"; + $this->migrationConfiguration['destinationIds']["destination_id_property_$i"] = array(); + } + $database_contents['migrate_map_sql_idmap_test'][] = $row; + $id_map = $this->getIdMap($database_contents); + // Test for a valid hit. + $destination_id = $id_map->lookupDestinationId($source_id_values); + $this->assertSame($expected_result, $destination_id); + // Test for a miss. + $destination_id = $id_map->lookupDestinationId($nonexistent_id_values); + $this->assertSame(0, count($destination_id)); + } + + /** + * Tests the getRowByDestination method. + */ + public function testGetRowByDestination() { + $row = array( + 'sourceid1' => 'source_id_value_1', + 'sourceid2' => 'source_id_value_2', + 'destid1' => 'destination_id_value_1', + ) + $this->idMapDefaults(); + $database_contents['migrate_map_sql_idmap_test'][] = $row; + $row = array( + 'sourceid1' => 'source_id_value_3', + 'sourceid2' => 'source_id_value_4', + 'destid1' => 'destination_id_value_2', + ) + $this->idMapDefaults(); + $database_contents['migrate_map_sql_idmap_test'][] = $row; + $dest_id_values = array($row['destid1']); + $id_map = $this->getIdMap($database_contents); + $result_row = $id_map->getRowByDestination($dest_id_values); + $this->assertSame($row, $result_row); + // This value does not exist. + $dest_id_values = array('invalid_destination_id_property'); + $id_map = $this->getIdMap($database_contents); + $result_row = $id_map->getRowByDestination($dest_id_values); + $this->assertFalse($result_row); + } + + /** + * Tests the source ID lookup method. + * + * Scenarios to test (for both hits and misses) are: + * - Single-value destination ID to single-value source ID. + * - Multi-value destination ID to multi-value source ID. + * - Single-value destination ID to multi-value source ID. + * - Multi-value destination ID to single-value source ID. + */ + public function testLookupSourceIDMapping() { + $this->performLookupSourceIdTest(1, 1); + $this->performLookupSourceIdTest(2, 2); + $this->performLookupSourceIdTest(1, 2); + $this->performLookupSourceIdTest(2, 1); + } + + /** + * Performs the source ID test on source and destination fields. + * + * @param int $num_source_fields + * Number of source fields to test. + * @param int $num_destination_fields + * Number of destination fields to test. + */ + protected function performLookupSourceIdTest($num_source_fields, $num_destination_fields) { + // Adjust the migration configuration according to the number of source and + // destination fields. + $this->migrationConfiguration['sourceIds'] = array(); + $this->migrationConfiguration['destinationIds'] = array(); + $row = $this->idMapDefaults(); + $expected_result = array(); + for ($i = 1; $i <= $num_source_fields; $i++) { + $row["sourceid$i"] = "source_id_value_$i"; + $expected_result[] = "source_id_value_$i"; + $this->migrationConfiguration['sourceIds']["source_id_property_$i"] = array(); + } + $destination_id_values = array(); + $nonexistent_id_values = array(); + for ($i = 1; $i <= $num_destination_fields; $i++) { + $row["destid$i"] = "destination_id_value_$i"; + $destination_id_values[] = "destination_id_value_$i"; + $nonexistent_id_values[] = "nonexistent_destination_id_value_$i"; + $this->migrationConfiguration['destinationIds']["destination_id_property_$i"] = array(); + } + $database_contents['migrate_map_sql_idmap_test'][] = $row; + $id_map = $this->getIdMap($database_contents); + // Test for a valid hit. + $source_id = $id_map->lookupSourceID($destination_id_values); + $this->assertSame($expected_result, $source_id); + // Test for a miss. + $source_id = $id_map->lookupSourceID($nonexistent_id_values); + $this->assertSame(0, count($source_id)); + } + + /** + * Tests the imported count method. + * + * Scenarios to test for: + * - No imports. + * - One import. + * - Multiple imports. + */ + public function testImportedCount() { + $id_map = $this->getIdMap(); + // Add a single failed row and assert zero imported rows. + $source = array('source_id_property' => 'source_value_failed'); + $row = new Row($source, array('source_id_property' => array())); + $destination = array('destination_id_property' => 'destination_value_failed'); + $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_FAILED); + $imported_count = $id_map->importedCount(); + $this->assertSame(0, $imported_count); + + // Add an imported row and assert single count. + $source = array('source_id_property' => 'source_value_imported'); + $row = new Row($source, array('source_id_property' => array())); + $destination = array('destination_id_property' => 'destination_value_imported'); + $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_IMPORTED); + $imported_count = $id_map->importedCount(); + $this->assertSame(1, $imported_count); + + // Add a row needing update and assert multiple imported rows. + $source = array('source_id_property' => 'source_value_update'); + $row = new Row($source, array('source_id_property' => array())); + $destination = array('destination_id_property' => 'destination_value_update'); + $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_NEEDS_UPDATE); + $imported_count = $id_map->importedCount(); + $this->assertSame(2, $imported_count); + } + + /** + * Tests the number of processed source rows. + * + * Scenarios to test for: + * - No processed rows. + * - One processed row. + * - Multiple processed rows. + */ + public function testProcessedCount() { + $id_map = $this->getIdMap(); + // Assert zero rows have been processed before adding rows. + $processed_count = $id_map->processedCount(); + $this->assertSame(0, $processed_count); + $row_statuses = array( + MigrateIdMapInterface::STATUS_IMPORTED, + MigrateIdMapInterface::STATUS_NEEDS_UPDATE, + MigrateIdMapInterface::STATUS_IGNORED, + MigrateIdMapInterface::STATUS_FAILED, + ); + // Create a mapping row for each STATUS constant. + foreach ($row_statuses as $status) { + $source = array('source_id_property' => 'source_value_' . $status); + $row = new Row($source, array('source_id_property' => array())); + $destination = array('destination_id_property' => 'destination_value_' . $status); + $id_map->saveIdMapping($row, $destination, $status); + if ($status == MigrateIdMapInterface::STATUS_IMPORTED) { + // Assert a single row has been processed. + $processed_count = $id_map->processedCount(); + $this->assertSame(1, $processed_count); + } + } + // Assert multiple rows have been processed. + $processed_count = $id_map->processedCount(); + $this->assertSame(count($row_statuses), $processed_count); + } + + /** + * Tests the update count method. + * + * Scenarios to test for: + * - No updates. + * - One update. + * - Multiple updates. + */ + public function testUpdateCount() { + $this->performUpdateCountTest(0); + $this->performUpdateCountTest(1); + $this->performUpdateCountTest(3); + } + + /** + * Performs the update count test with a given number of update rows. + * + * @param int $num_update_rows + * The number of update rows to test. + */ + protected function performUpdateCountTest($num_update_rows) { + $database_contents['migrate_map_sql_idmap_test'] = array(); + for ($i = 0; $i < 5; $i++) { + $row = $this->idMapDefaults(); + $row['sourceid1'] = "source_id_value_$i"; + $row['destid1'] = "destination_id_value_$i"; + $row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED; + $database_contents['migrate_map_sql_idmap_test'][] = $row; + } + for (; $i < 5 + $num_update_rows; $i++) { + $row = $this->idMapDefaults(); + $row['sourceid1'] = "source_id_value_$i"; + $row['destid1'] = "destination_id_value_$i"; + $row['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE; + $database_contents['migrate_map_sql_idmap_test'][] = $row; + } + $id_map = $this->getIdMap($database_contents); + $count = $id_map->updateCount(); + $this->assertSame($num_update_rows, $count); + } + + /** + * Tests the error count method. + * + * Scenarios to test for: + * - No errors. + * - One error. + * - Multiple errors. + */ + public function testErrorCount() { + $this->performErrorCountTest(0); + $this->performErrorCountTest(1); + $this->performErrorCountTest(3); + } + + /** + * Performs error count test with a given number of error rows. + * + * @param int $num_error_rows + * Number of error rows to test. + */ + protected function performErrorCountTest($num_error_rows) { + $database_contents['migrate_map_sql_idmap_test'] = array(); + for ($i = 0; $i < 5; $i++) { + $row = $this->idMapDefaults(); + $row['sourceid1'] = "source_id_value_$i"; + $row['destid1'] = "destination_id_value_$i"; + $row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED; + $database_contents['migrate_map_sql_idmap_test'][] = $row; + } + for (; $i < 5 + $num_error_rows; $i++) { + $row = $this->idMapDefaults(); + $row['sourceid1'] = "source_id_value_$i"; + $row['destid1'] = "destination_id_value_$i"; + $row['source_row_status'] = MigrateIdMapInterface::STATUS_FAILED; + $database_contents['migrate_map_sql_idmap_test'][] = $row; + } + + $id_map = $this->getIdMap($database_contents); + $count = $id_map->errorCount(); + $this->assertSame($num_error_rows, $count); + } + + /** + * Tests setting a row source_row_status to STATUS_NEEDS_UPDATE. + */ + public function testSetUpdate() { + $id_map = $this->getIdMap(); + $row_statuses = array( + MigrateIdMapInterface::STATUS_IMPORTED, + MigrateIdMapInterface::STATUS_NEEDS_UPDATE, + MigrateIdMapInterface::STATUS_IGNORED, + MigrateIdMapInterface::STATUS_FAILED, + ); + // Create a mapping row for each STATUS constant. + foreach ($row_statuses as $status) { + $source = array('source_id_property' => 'source_value_' . $status); + $row = new Row($source, array('source_id_property' => array())); + $destination = array('destination_id_property' => 'destination_value_' . $status); + $id_map->saveIdMapping($row, $destination, $status); + $expected_results[] = array( + 'sourceid1' => 'source_value_' . $status, + 'destid1' => 'destination_value_' . $status, + 'source_row_status' => $status, + 'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE, + 'hash' => '', + ); + } + // Assert that test values exist. + $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_results); + // Mark each row as STATUS_NEEDS_UPDATE. + foreach ($row_statuses as $status) { + $id_map->setUpdate(array('source_value_' . $status)); + } + // Update expected results. + foreach ($expected_results as $key => $value) { + $expected_results[$key]['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE; + } + // Assert that updated expected values match. + $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_results); + // Assert an exception is thrown when source identifiers are not provided. + try { + $id_map->setUpdate(array()); + $this->assertFalse(FALSE, 'MigrateException not thrown, when source identifiers were provided to update.'); + } + catch (MigrateException $e) { + $this->assertTrue(TRUE, "MigrateException thrown, when source identifiers were not provided to update."); + } + } + + /** + * Tests prepareUpdate(). + */ + public function testPrepareUpdate() { + $id_map = $this->getIdMap(); + $row_statuses = array( + MigrateIdMapInterface::STATUS_IMPORTED, + MigrateIdMapInterface::STATUS_NEEDS_UPDATE, + MigrateIdMapInterface::STATUS_IGNORED, + MigrateIdMapInterface::STATUS_FAILED, + ); + + // Create a mapping row for each STATUS constant. + foreach ($row_statuses as $status) { + $source = array('source_id_property' => 'source_value_' . $status); + $row = new Row($source, array('source_id_property' => array())); + $destination = array('destination_id_property' => 'destination_value_' . $status); + $id_map->saveIdMapping($row, $destination, $status); + $expected_results[] = array( + 'sourceid1' => 'source_value_' . $status, + 'destid1' => 'destination_value_' . $status, + 'source_row_status' => $status, + 'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE, + 'hash' => '', + ); + } + + // Assert that test values exist. + $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_results); + + // Mark all rows as STATUS_NEEDS_UPDATE. + $id_map->prepareUpdate(); + + // Update expected results. + foreach ($expected_results as $key => $value) { + $expected_results[$key]['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE; + } + // Assert that updated expected values match. + $this->queryResultTest($this->database->databaseContents['migrate_map_sql_idmap_test'], $expected_results); + } + + /** + * Tests the destroy method. + * + * Scenarios to test for: + * - No errors. + * - One error. + * - Multiple errors. + */ + public function testDestroy() { + $id_map = $this->getIdMap(); + $map_table_name = $id_map->getMapTableName(); + $message_table_name = $id_map->getMessageTableName(); + $row = new Row(array('source_id_property' => 'source_value'), array('source_id_property' => array())); + $id_map->saveIdMapping($row, array('destination_id_property' => 2)); + $id_map->saveMessage(array('source_value'), 'A message'); + $this->assertTrue($this->database->schema()->tableExists($map_table_name), + "$map_table_name exists"); + $this->assertTrue($this->database->schema()->tableExists($message_table_name), + "$message_table_name exists"); + $id_map->destroy(); + $this->assertFalse($this->database->schema()->tableExists($map_table_name), + "$map_table_name does not exist"); + $this->assertFalse($this->database->schema()->tableExists($message_table_name), + "$message_table_name does not exist"); + } + + /** + * Tests the getQualifiedMapTable method with an unprefixed database. + */ + public function testGetQualifiedMapTableNoPrefix() { + $id_map = $this->getIdMap(array(), array('database' => 'source_database')); + $qualified_map_table = $id_map->getQualifiedMapTableName(); + $this->assertEquals('source_database.migrate_map_sql_idmap_test', $qualified_map_table); + } + + /** + * Tests the getQualifiedMapTable method with a prefixed database. + */ + public function testGetQualifiedMapTablePrefix() { + $id_map = $this->getIdMap(array(), array('database' => 'source_database'), 'prefix'); + $qualified_map_table = $id_map->getQualifiedMapTableName(); + $this->assertEquals('migrate_map_sql_idmap_test', $qualified_map_table); + } + + /** + * Tests all the iterator methods in one swing. + * + * The iterator methods are: + * - Sql::rewind() + * - Sql::next() + * - Sql::valid() + * - Sql::key() + * - Sql::current() + */ + public function testIterators() { + $database_contents['migrate_map_sql_idmap_test'] = array(); + for ($i = 0; $i < 3; $i++) { + $row = $this->idMapDefaults(); + $row['sourceid1'] = "source_id_value_$i"; + $row['destid1'] = "destination_id_value_$i"; + $row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED; + $expected_results[serialize(array('sourceid1' => $row['sourceid1']))] = array('destid1' => $row['destid1']); + $database_contents['migrate_map_sql_idmap_test'][] = $row; + } + + $id_map = $this->getIdMap($database_contents); + $this->assertSame(iterator_to_array($id_map), $expected_results); + } + +} diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateTestCase.php b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateTestCase.php index 40d9f639de4e..ef94410ebc46 100644 --- a/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateTestCase.php +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/MigrateTestCase.php @@ -30,9 +30,10 @@ abstract class MigrateTestCase extends UnitTestCase { * The mocked migration. */ protected function getMigration() { - $idmap = $this->getMock('Drupal\migrate\Plugin\MigrateIdMapInterface'); + $this->idMap = $this->getMock('Drupal\migrate\Plugin\MigrateIdMapInterface'); + if ($this->mapJoinable) { - $idmap->expects($this->once()) + $this->idMap->expects($this->once()) ->method('getQualifiedMapTableName') ->will($this->returnValue('test_map')); } @@ -40,11 +41,14 @@ protected function getMigration() { $migration = $this->getMock('Drupal\migrate\Entity\MigrationInterface'); $migration->expects($this->any()) ->method('getIdMap') - ->will($this->returnValue($idmap)); - $configuration = $this->migrationConfiguration; - $migration->expects($this->any())->method('get')->will($this->returnCallback(function ($argument) use ($configuration) { + ->will($this->returnValue($this->idMap)); + $configuration = &$this->migrationConfiguration; + $migration->expects($this->any())->method('get')->will($this->returnCallback(function ($argument) use (&$configuration) { return isset($configuration[$argument]) ? $configuration[$argument] : ''; })); + $migration->expects($this->any())->method('set')->will($this->returnCallback(function ($argument, $value) use (&$configuration) { + $configuration[$argument] = $value; + })); $migration->expects($this->any()) ->method('id') ->will($this->returnValue($configuration['id'])); @@ -52,45 +56,21 @@ protected function getMigration() { } /** - * @return \Drupal\Core\Database\Connection + * Get a fake database connection object for use in tests. + * + * @param array $database_contents + * The database contents faked as an array. Each key is a table name, each + * value is a list of table rows, an associative array of field => value. + * @param array $connection_options + * (optional) The array of connection options for the database. + * @param string $prefix + * (optional) The table prefix on the database. + * + * @return \Drupal\migrate\Tests\FakeConnection + * The database connection. */ - protected function getDatabase($database_contents) { - $database = $this->getMockBuilder('Drupal\Core\Database\Connection') - ->disableOriginalConstructor() - ->getMock(); - $database->databaseContents = &$database_contents; - - // Although select doesn't modify the contents of the database, it still - // needs to be a reference so that we can select previously inserted or - // updated rows. - $database->expects($this->any()) - ->method('select')->will($this->returnCallback(function ($base_table, $base_alias) use (&$database_contents) { - return new FakeSelect($base_table, $base_alias, $database_contents); - })); - $database->expects($this->any()) - ->method('schema') - ->will($this->returnCallback(function () use (&$database_contents) { - return new FakeDatabaseSchema($database_contents); - })); - $database->expects($this->any()) - ->method('insert') - ->will($this->returnCallback(function ($table) use (&$database_contents) { - return new FakeInsert($database_contents, $table); - })); - $database->expects($this->any()) - ->method('update') - ->will($this->returnCallback(function ($table) use (&$database_contents) { - return new FakeUpdate($database_contents, $table); - })); - $database->expects($this->any()) - ->method('merge') - ->will($this->returnCallback(function ($table) use (&$database_contents) { - return new FakeMerge($database_contents, $table); - })); - $database->expects($this->any()) - ->method('query') - ->will($this->throwException(new \Exception('Query is not supported'))); - return $database; + protected function getDatabase(array $database_contents, $connection_options = array(), $prefix = '') { + return new FakeConnection($database_contents, $connection_options, $prefix); } /** diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/RowTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/RowTest.php new file mode 100644 index 000000000000..2baf52a3af53 --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/RowTest.php @@ -0,0 +1,260 @@ +<?php + +/** + * @file + * Contains \Drupal\migrate\Tests\RowTest. + */ + +namespace Drupal\migrate\Tests; + +use Drupal\migrate\Plugin\MigrateIdMapInterface; +use Drupal\migrate\Row; +use Drupal\Tests\UnitTestCase; + +/** + * Tests for Row class. + * + * @group Drupal + * @group migrate + * + * @covers \Drupal\migrate\Row + */ +class RowTest extends UnitTestCase { + + /** + * The source IDs. + * + * @var array + */ + protected $testSourceIds = array( + 'nid' => 'Node ID', + ); + + /** + * The test values. + * + * @var array + */ + protected $testValues = array( + 'nid' => 1, + 'title' => 'node 1', + ); + + /** + * The test hash. + * + * @var string + */ + protected $testHash = '85795d4cde4a2425868b812cc88052ecd14fc912e7b9b4de45780f66750e8b1e'; + + /** + * The test hash after changing title value to 'new title'. + * + * @var string + */ + protected $testHashMod = '9476aab0b62b3f47342cc6530441432e5612dcba7ca84115bbab5cceaca1ecb3'; + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'Row class functionality', + 'description' => 'Tests Row class functionality.', + 'group' => 'Migrate', + ); + } + + /** + * Tests object creation: empty. + */ + public function testRowWithoutData() { + $row = new Row(array(), array()); + $this->assertSame(array(), $row->getSource(), 'Empty row'); + } + + /** + * Tests object creation: basic. + */ + public function testRowWithBasicData() { + $row = new Row($this->testValues, $this->testSourceIds); + $this->assertSame($this->testValues, $row->getSource(), 'Row with data, simple id.'); + } + + /** + * Tests object creation: multiple source IDs. + */ + public function testRowWithMultipleSourceIds() { + $multi_source_ids = $this->testSourceIds + array('vid' => 'Node revision'); + $multi_source_ids_values = $this->testValues + array('vid' => 1); + $row = new Row($multi_source_ids_values, $multi_source_ids); + $this->assertSame($multi_source_ids_values, $row->getSource(), 'Row with data, multifield id.'); + } + + /** + * Tests object creation: invalid values. + * + * @expectedException \Exception + */ + public function testRowWithInvalidData() { + $invalid_values = array( + 'title' => 'node X', + ); + $row = new Row($invalid_values, $this->testSourceIds); + } + + /** + * Tests source immutability after freeze. + * + * @expectedException \Exception + */ + public function testSourceFreeze() { + $row = new Row($this->testValues, $this->testSourceIds); + $row->rehash(); + $this->assertSame($this->testHash, $row->getHash(), 'Correct hash.'); + $row->setSourceProperty('title', 'new title'); + $row->rehash(); + $this->assertSame($this->testHashMod, $row->getHash(), 'Hash changed correctly.'); + $row->freezeSource(); + $row->setSourceProperty('title', 'new title'); + } + + /** + * Tests setting on a frozen row. + * + * @expectedException \Exception + * @expectedExceptionMessage The source is frozen and can't be changed any more + */ + public function testSetFrozenRow() { + $row = new Row($this->testValues, $this->testSourceIds); + $row->freezeSource(); + $row->setSourceProperty('title', 'new title'); + } + + /** + * Tests hashing. + */ + public function testHashing() { + $row = new Row($this->testValues, $this->testSourceIds); + $this->assertSame('', $row->getHash(), 'No hash at creation'); + $row->rehash(); + $this->assertSame($this->testHash, $row->getHash(), 'Correct hash.'); + $row->rehash(); + $this->assertSame($this->testHash, $row->getHash(), 'Correct hash even doing it twice.'); + + // Set the map to needs update. + $test_id_map = array( + 'original_hash' => '', + 'hash' => '', + 'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE, + ); + $row->setIdMap($test_id_map); + $this->assertTrue($row->needsUpdate()); + + $row->rehash(); + $this->assertSame($this->testHash, $row->getHash(), 'Correct hash even if id_mpa have changed.'); + $row->setSourceProperty('title', 'new title'); + $row->rehash(); + $this->assertSame($this->testHashMod, $row->getHash(), 'Hash changed correctly.'); + + // Set the map to successfully imported. + $test_id_map = array( + 'original_hash' => '', + 'hash' => '', + 'source_row_status' => MigrateIdMapInterface::STATUS_IMPORTED, + ); + $row->setIdMap($test_id_map); + $this->assertFalse($row->needsUpdate()); + + // Set the same hash value and ensure it was not changed. + $random = $this->randomName(); + $test_id_map = array( + 'original_hash' => $random, + 'hash' => $random, + 'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE, + ); + $row->setIdMap($test_id_map); + $this->assertFalse($row->changed()); + + // Set different has values to ensure it is marked as changed. + $test_id_map = array( + 'original_hash' => $this->randomName(), + 'hash' => $this->randomName(), + 'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE, + ); + $row->setIdMap($test_id_map); + $this->assertTrue($row->changed()); + } + + /** + * Tests getting/setting the ID Map. + * + * @covers \Drupal\migrate\Row::setIdMap() + * @covers \Drupal\migrate\Row::getIdMap() + */ + public function testGetSetIdMap() { + $row = new Row($this->testValues, $this->testSourceIds); + $test_id_map = array( + 'original_hash' => '', + 'hash' => '', + 'source_row_status' => MigrateIdMapInterface::STATUS_NEEDS_UPDATE, + ); + $row->setIdMap($test_id_map); + $this->assertEquals($test_id_map, $row->getIdMap()); + } + + /** + * Tests the source ID. + */ + public function testSourceIdValues() { + $row = new Row($this->testValues, $this->testSourceIds); + $this->assertSame(array('nid' => $this->testValues['nid']), $row->getSourceIdValues()); + } + + /** + * Tests getting the source property. + * + * @covers \Drupal\migrate\Row::getSourceProperty() + */ + public function testGetSourceProperty() { + $row = new Row($this->testValues, $this->testSourceIds); + $this->assertSame($this->testValues['nid'], $row->getSourceProperty('nid')); + $this->assertSame($this->testValues['title'], $row->getSourceProperty('title')); + $this->assertNull($row->getSourceProperty('non_existing')); + } + + /** + * Tests setting and getting the destination. + */ + public function testDestination() { + $row = new Row($this->testValues, $this->testSourceIds); + $this->assertEmpty($row->getDestination()); + $this->assertFalse($row->hasDestinationProperty('nid')); + + // Set a destination. + $row->setDestinationProperty('nid', 2); + $this->assertTrue($row->hasDestinationProperty('nid')); + $this->assertEquals(array('nid' => 2), $row->getDestination()); + } + + /** + * Tests setting/getting multiple destination IDs. + */ + public function testMultipleDestination() { + $row = new Row($this->testValues, $this->testSourceIds); + // Set some deep nested values. + $row->setDestinationProperty('image:alt', 'alt text'); + $row->setDestinationProperty('image:fid', 3); + + $this->assertTrue($row->hasDestinationProperty('image')); + $this->assertFalse($row->hasDestinationProperty('alt')); + $this->assertFalse($row->hasDestinationProperty('fid')); + + $destination = $row->getDestination(); + $this->assertEquals('alt text', $destination['image']['alt']); + $this->assertEquals(3, $destination['image']['fid']); + $this->assertEquals('alt text', $row->getDestinationProperty('image:alt')); + $this->assertEquals(3, $row->getDestinationProperty('image:fid')); + } + +} diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/TestMigrateExecutable.php b/core/modules/migrate/tests/Drupal/migrate/Tests/TestMigrateExecutable.php new file mode 100644 index 000000000000..1a222c77df13 --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/TestMigrateExecutable.php @@ -0,0 +1,231 @@ +<?php + +/** + * @file + * Contains \Drupal\migrate\Tests\TestMigrateExecutable. + */ + +namespace Drupal\migrate\Tests; + +use Drupal\Core\StringTranslation\TranslationInterface; +use Drupal\migrate\MigrateExecutable; + +/** + * Tests MigrateExecutable. + */ +class TestMigrateExecutable extends MigrateExecutable { + + /** + * The (fake) number of seconds elapsed since the start of the test. + * + * @var int + */ + protected $timeElapsed; + + /** + * The fake memory usage in bytes. + * + * @var int + */ + protected $memoryUsage; + + /** + * The cleared memory usage. + * + * @var int + */ + protected $clearedMemoryUsage; + + /** + * Sets the translation manager. + * + * @param \Drupal\Core\StringTranslation\TranslationInterface $translation_manager + * The translation manager. + */ + public function setTranslationManager(TranslationInterface $translation_manager) { + $this->translationManager = $translation_manager; + } + + /** + * Allows access to protected timeOptionExceeded method. + * + * @return bool + * A threshold exceeded value. + */ + public function timeOptionExceeded() { + return parent::timeOptionExceeded(); + } + + /** + * Allows access to set protected maxExecTime property. + * + * @param int $max_exec_time + * The value to set. + */ + public function setMaxExecTime($max_exec_time) { + $this->maxExecTime = $max_exec_time; + } + + /** + * Allows access to protected maxExecTime property. + * + * @return int + * The value of the protected property. + */ + public function getMaxExecTime() { + return $this->maxExecTime; + } + + /** + * Allows access to protected successesSinceFeedback property. + * + * @return int + * The value of the protected property. + */ + public function getSuccessesSinceFeedback() { + return $this->successesSinceFeedback; + } + + /** + * Allows access to protected totalSuccesses property. + * + * @return int + * The value of the protected property. + */ + public function getTotalSuccesses() { + return $this->totalSuccesses; + } + + /** + * Allows access to protected totalProcessed property. + * + * @return int + * The value of the protected property. + */ + public function getTotalProcessed() { + return $this->totalProcessed; + } + + /** + * Allows access to protected processedSinceFeedback property. + * + * @var int + * The value of the protected property. + */ + public function getProcessedSinceFeedback() { + return $this->processedSinceFeedback; + } + + /** + * Allows access to protected maxExecTimeExceeded method. + * + * @return bool + * The threshold exceeded value. + */ + public function maxExecTimeExceeded() { + return parent::maxExecTimeExceeded(); + } + + /** + * Allows access to set protected source property. + * + * @param \Drupal\migrate\Source $source + * The value to set. + */ + public function setSource($source) { + $this->source = $source; + } + + /** + * Allows access to protected sourceIdValues property. + * + * @param array $source_id_values + * The value to set. + */ + public function setSourceIdValues($source_id_values) { + $this->sourceIdValues = $source_id_values; + } + + /** + * Allows setting a fake elapsed time. + * + * @param int $time + * The time in seconds. + */ + public function setTimeElapsed($time) { + $this->timeElapsed = $time; + } + + /** + * {@inheritdoc} + */ + public function getTimeElapsed() { + return $this->timeElapsed; + } + + /** + * {@inheritdoc} + */ + public function handleException(\Exception $exception, $save = TRUE) { + $message = $exception->getMessage(); + if ($save) { + $this->saveMessage($message); + } + $this->message->display($message); + } + + /** + * Allows access to the protected memoryExceeded method. + * + * @return bool + * The memoryExceeded value. + */ + public function memoryExceeded() { + return parent::memoryExceeded(); + } + + /** + * {@inheritdoc} + */ + protected function attemptMemoryReclaim() { + return $this->clearedMemoryUsage; + } + + /** + * {@inheritdoc} + */ + protected function getMemoryUsage() { + return $this->memoryUsage; + } + + /** + * Sets the fake memory usage. + * + * @param int $memory_usage + * The fake memory usage value. + * @param int $cleared_memory_usage + * (optional) The fake cleared memory value. + */ + public function setMemoryUsage($memory_usage, $cleared_memory_usage = NULL) { + $this->memoryUsage = $memory_usage; + $this->clearedMemoryUsage = $cleared_memory_usage; + } + + /** + * Sets the memory limit. + * + * @param int $memory_limit + * The memory limit. + */ + public function setMemoryLimit($memory_limit) { + $this->memoryLimit = $memory_limit; + } + + /** + * {@inheritdoc} + */ + protected function formatSize($size) { + return $size; + } + +} diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/TestSqlIdMap.php b/core/modules/migrate/tests/Drupal/migrate/Tests/TestSqlIdMap.php new file mode 100644 index 000000000000..76f59dbc64f2 --- /dev/null +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/TestSqlIdMap.php @@ -0,0 +1,45 @@ +<?php + +/** + * @file + * Contains \Drupal\migrate\Tests\TestSqlIdMap. + */ + +namespace Drupal\migrate\Tests; + +use Drupal\Core\Database\Connection; +use Drupal\migrate\Entity\MigrationInterface; +use Drupal\migrate\Plugin\migrate\id_map\Sql; + +/** + * Defines a SQL ID map for use in tests. + */ +class TestSqlIdMap extends Sql implements \Iterator { + + /** + * Constructs a TestSqlIdMap object. + * + * @param \Drupal\Core\Database\Connection $database + * The database. + * @param array $configuration + * The configuration. + * @param string $plugin_id + * The plugin ID for the migration process to do. + * @param array $plugin_definition + * The configuration for the plugin. + * @param \Drupal\migrate\Entity\MigrationInterface $migration + * The migration to do. + */ + function __construct(Connection $database, array $configuration, $plugin_id, array $plugin_definition, MigrationInterface $migration) { + $this->database = $database; + parent::__construct($configuration, $plugin_id, $plugin_definition, $migration); + } + + /** + * {@inheritdoc} + */ + public function getDatabase() { + return parent::getDatabase(); + } + +} diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/process/GetTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/process/GetTest.php index e49e1f2cc5c4..60e66a77993b 100644 --- a/core/modules/migrate/tests/Drupal/migrate/Tests/process/GetTest.php +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/process/GetTest.php @@ -25,11 +25,17 @@ public static function getInfo() { ); } + /** + * {@inheritdoc} + */ function setUp() { $this->plugin = new TestGet(); parent::setUp(); } + /** + * Tests the Get plugin when source is a string. + */ function testTransformSourceString() { $this->row->expects($this->once()) ->method('getSourceProperty') @@ -40,6 +46,9 @@ function testTransformSourceString() { $this->assertSame($value, 'source_value'); } + /** + * Tests the Get plugin when source is an array. + */ function testTransformSourceArray() { $map = array( 'test1' => 'source_value1', @@ -53,6 +62,9 @@ function testTransformSourceArray() { $this->assertSame($value, array('source_value1', 'source_value2')); } + /** + * Tests the Get plugin when source is a string pointing to destination. + */ function testTransformSourceStringAt() { $this->row->expects($this->once()) ->method('getSourceProperty') @@ -63,6 +75,9 @@ function testTransformSourceStringAt() { $this->assertSame($value, 'source_value'); } + /** + * Tests the Get plugin when source is an array pointing to destination. + */ function testTransformSourceArrayAt() { $map = array( 'test1' => 'source_value1', diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/process/IteratorTest.php b/core/modules/migrate/tests/Drupal/migrate/Tests/process/IteratorTest.php index 39aeb545f9c9..454206fb30a9 100644 --- a/core/modules/migrate/tests/Drupal/migrate/Tests/process/IteratorTest.php +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/process/IteratorTest.php @@ -9,6 +9,7 @@ use Drupal\migrate\MigrateExecutable; use Drupal\migrate\Plugin\migrate\process\Get; use Drupal\migrate\Plugin\migrate\process\Iterator; +use Drupal\migrate\Plugin\migrate\process\StaticMap; use Drupal\migrate\Row; use Drupal\migrate\Tests\MigrateTestCase; @@ -53,7 +54,7 @@ public static function getInfo() { /** * Tests the iterator process plugin. */ - function testIterator() { + public function testIterator() { $migration = $this->getMigration(); // Set up the properties for the iterator. $configuration = array( diff --git a/core/modules/migrate/tests/Drupal/migrate/Tests/process/MigrateProcessTestCase.php b/core/modules/migrate/tests/Drupal/migrate/Tests/process/MigrateProcessTestCase.php index d99a865b2ab2..923ca97ee5d6 100644 --- a/core/modules/migrate/tests/Drupal/migrate/Tests/process/MigrateProcessTestCase.php +++ b/core/modules/migrate/tests/Drupal/migrate/Tests/process/MigrateProcessTestCase.php @@ -12,7 +12,7 @@ abstract class MigrateProcessTestCase extends MigrateTestCase { /** - * @var \Drupal\migrate\Plugin\migrate\process\TestGet + * @var \Drupal\migrate\Plugin\MigrateProcessInterface */ protected $plugin; diff --git a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php index 5c6526b02d89..19cabeb2dbf9 100644 --- a/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php +++ b/core/modules/migrate_drupal/lib/Drupal/migrate_drupal/Tests/d6/MigrateUserRoleTest.php @@ -38,7 +38,7 @@ function testUserRole() { $migrate_test_role_1 = entity_load('user_role', $rid); $this->assertEqual($migrate_test_role_1->id(), $rid); $this->assertEqual($migrate_test_role_1->getPermissions(), array(0 => 'migrate test role 1 test permission')); - $this->assertEqual(array($rid), $migration->getIdMap()->lookupDestinationID(array(3))); + $this->assertEqual(array($rid), $migration->getIdMap()->lookupDestinationId(array(3))); $rid = 'migrate_test_role_2'; $migrate_test_role_2 = entity_load('user_role', $rid); $this->assertEqual($migrate_test_role_2->getPermissions(), array( @@ -59,7 +59,7 @@ function testUserRole() { 'access content overview', )); $this->assertEqual($migrate_test_role_2->id(), $rid); - $this->assertEqual(array($rid), $migration->getIdMap()->lookupDestinationID(array(4))); + $this->assertEqual(array($rid), $migration->getIdMap()->lookupDestinationId(array(4))); } } -- GitLab