diff --git a/migrate_tools.routing.yml b/migrate_tools.routing.yml index b12735a..edb4965 100644 --- a/migrate_tools.routing.yml +++ b/migrate_tools.routing.yml @@ -74,6 +74,13 @@ entity.migration.process: _title: 'Process' requirements: _permission: 'administer migrations' +entity.migration.process.run: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/process/run' + defaults: + _controller: '\Drupal\migrate_tools\Controller\MigrationController::run' + _title: 'Run' + requirements: + _permission: 'administer migrations' entity.migration.destination: path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/destination' defaults: @@ -107,3 +114,11 @@ migrate_tools.messages: _title: 'Messages' requirements: _permission: 'administer migrations' + +migrate_tools.execute: + path: '/admin/structure/migrate/manage/{migration_group}/migrations/{migration}/execute' + defaults: + _form: '\Drupal\migrate_tools\Form\MigrationExecuteForm' + _title: 'Execute migration' + requirements: + _permission: 'administer migrations' diff --git a/src/Controller/MigrationController.php b/src/Controller/MigrationController.php index 15905cb..6004c3e 100644 --- a/src/Controller/MigrationController.php +++ b/src/Controller/MigrationController.php @@ -5,9 +5,13 @@ use Drupal\Core\Controller\ControllerBase; use Drupal\Component\Utility\Xss; use Drupal\Component\Utility\Html; +use Drupal\Core\Routing\CurrentRouteMatch; use Drupal\migrate\Plugin\MigrationPluginManagerInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\migrate_tools\MigrateBatchExecutable; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Url; +use Drupal\migrate\MigrateMessage; /** * Returns responses for migrate_tools migration view routes. @@ -22,13 +26,23 @@ class MigrationController extends ControllerBase implements ContainerInjectionIn protected $migrationPluginManager; /** + * The current route match. + * + * @var \Drupal\Core\Routing\CurrentRouteMatch + */ + protected $currentRouteMatch; + + /** * Constructs a new MigrationController object. * * @param \Drupal\migrate\Plugin\MigrationPluginManagerInterface $migration_plugin_manager * The plugin manager for config entity-based migrations. + * @param \Drupal\Core\Routing\CurrentRouteMatch $currentRouteMatch + * The current route match. */ - public function __construct(MigrationPluginManagerInterface $migration_plugin_manager) { + public function __construct(MigrationPluginManagerInterface $migration_plugin_manager, CurrentRouteMatch $currentRouteMatch) { $this->migrationPluginManager = $migration_plugin_manager; + $this->currentRouteMatch = $currentRouteMatch; } /** @@ -36,7 +50,8 @@ public function __construct(MigrationPluginManagerInterface $migration_plugin_ma */ public static function create(ContainerInterface $container) { return new static( - $container->get('plugin.manager.migration') + $container->get('plugin.manager.migration'), + $container->get('current_route_match') ); } @@ -145,6 +160,35 @@ public function source($migration_group, $migration) { } /** + * Run a migration. + * + * @param string $migration_group + * Machine name of the migration's group. + * @param string $migration + * Machine name of the migration. + * + * @return array + * A render array as expected by drupal_render(). + */ + public function run($migration_group, $migration) { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance($migration); + + $migrateMessage = new MigrateMessage(); + $options = []; + + $executable = new MigrateBatchExecutable($migration, $migrateMessage, $options); + $executable->batchImport(); + + $migration_group = $this->currentRouteMatch->getParameter('migration_group'); + $route_parameters = [ + 'migration_group' => $migration_group, + 'migration' => $migration->id(), + ]; + return batch_process(Url::fromRoute('entity.migration.process', $route_parameters)); + } + + /** * Display process information of a migration entity. * * @param string $migration_group @@ -204,6 +248,12 @@ public function process($migration_group, $migration) { '#empty' => $this->t('No process defined.'), ]; + $build['process']['run'] = [ + '#type' => 'link', + '#title' => $this->t('Run'), + '#url' => Url::fromRoute('entity.migration.process.run', ['migration_group' => $migration_group, 'migration' => $migration->id()]), + ]; + return $build; } diff --git a/src/Controller/MigrationListBuilder.php b/src/Controller/MigrationListBuilder.php index 5fedb6c..b14fc45 100644 --- a/src/Controller/MigrationListBuilder.php +++ b/src/Controller/MigrationListBuilder.php @@ -2,6 +2,7 @@ namespace Drupal\migrate_tools\Controller; +use Drupal\Component\Plugin\Exception\PluginException; use Drupal\Core\Config\Entity\ConfigEntityListBuilder; use Drupal\Core\Entity\EntityHandlerInterface; use Drupal\Core\Entity\EntityInterface; @@ -99,7 +100,7 @@ protected function getEntityIds() { * @return array * A render array structure of header strings. * - * @see Drupal\Core\Entity\EntityListController::render() + * @see \Drupal\Core\Entity\EntityListController::render() */ public function buildHeader() { $header['label'] = $this->t('Migration'); @@ -110,6 +111,7 @@ public function buildHeader() { $header['unprocessed'] = $this->t('Unprocessed'); $header['messages'] = $this->t('Messages'); $header['last_imported'] = $this->t('Last Imported'); + $header['operations'] = $this->t('Operations'); return $header; } @@ -125,56 +127,86 @@ public function buildHeader() { * @see \Drupal\Core\Entity\EntityListController::render() */ public function buildRow(EntityInterface $migration_entity) { - $migration = $this->migrationPluginManager->createInstance($migration_entity->id()); - $migration_group = $migration->get('migration_group'); - if (!$migration_group) { - $migration_group = 'default'; + try { + /** @var \Drupal\migrate\Plugin\MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance($migration_entity->id()); + $migration_group = $migration->get('migration_group'); + if (!$migration_group) { + $migration_group = 'default'; + } + $route_parameters = [ + 'migration_group' => $migration_group, + 'migration' => $migration->id(), + ]; + $row['label'] = [ + 'data' => [ + '#type' => 'link', + '#title' => $migration->label(), + '#url' => Url::fromRoute("entity.migration.overview", $route_parameters), + ], + ]; + $row['machine_name'] = $migration->id(); + $row['status'] = $migration->getStatusLabel(); + } catch (PluginException $e) { + return NULL; } - $route_parameters = [ - 'migration_group' => $migration_group, - 'migration' => $migration->id(), - ]; - $row['label'] = [ - 'data' => [ - '#type' => 'link', - '#title' => $migration->label(), - '#url' => Url::fromRoute("entity.migration.overview", $route_parameters), - ], - ]; - $row['machine_name'] = $migration->id(); - $row['status'] = $migration->getStatusLabel(); - // Derive the stats. - $source_plugin = $migration->getSourcePlugin(); - $row['total'] = $source_plugin->count(); - $map = $migration->getIdMap(); - $row['imported'] = $map->importedCount(); - // -1 indicates uncountable sources. - if ($row['total'] == -1) { + try { + // Derive the stats. + $source_plugin = $migration->getSourcePlugin(); + $row['total'] = $source_plugin->count(); + $map = $migration->getIdMap(); + $row['imported'] = $map->importedCount(); + // -1 indicates uncountable sources. + if ($row['total'] == -1) { + $row['total'] = $this->t('N/A'); + $row['unprocessed'] = $this->t('N/A'); + } + else { + $row['unprocessed'] = $row['total'] - $map->processedCount(); + } + $row['messages'] = [ + 'data' => [ + '#type' => 'link', + '#title' => $map->messageCount(), + '#url' => Url::fromRoute("migrate_tools.messages", $route_parameters), + ], + ]; + $migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported'); + $last_imported = $migrate_last_imported_store->get($migration->id(), FALSE); + if ($last_imported) { + /** @var \Drupal\Core\Datetime\DateFormatter $date_formatter */ + $date_formatter = \Drupal::service('date.formatter'); + $row['last_imported'] = $date_formatter->format($last_imported / 1000, + 'custom', 'Y-m-d H:i:s'); + } + else { + $row['last_imported'] = ''; + } + + $row['operations']['data'] = [ + '#type' => 'dropbutton', + '#links' => [ + 'simple_form' => [ + 'title' => $this->t('Execute'), + 'url' => Url::fromRoute('migrate_tools.execute', [ + 'migration_group' => $migration_group, + 'migration' => $migration->id(), + ]), + ], + ], + ]; + } catch (PluginException $e) { + // Derive the stats. + $row['status'] = $this->t('No data found'); $row['total'] = $this->t('N/A'); + $row['imported'] = $this->t('N/A'); $row['unprocessed'] = $this->t('N/A'); + $row['messages'] = $this->t('N/A'); + $row['last_imported'] = $this->t('N/A'); + $row['operations'] = $this->t('N/A'); } - else { - $row['unprocessed'] = $row['total'] - $map->processedCount(); - } - $row['messages'] = [ - 'data' => [ - '#type' => 'link', - '#title' => $map->messageCount(), - '#url' => Url::fromRoute("migrate_tools.messages", $route_parameters), - ], - ]; - $migrate_last_imported_store = \Drupal::keyValue('migrate_last_imported'); - $last_imported = $migrate_last_imported_store->get($migration->id(), FALSE); - if ($last_imported) { - /** @var DateFormatter $date_formatter */ - $date_formatter = \Drupal::service('date.formatter'); - $row['last_imported'] = $date_formatter->format($last_imported / 1000, - 'custom', 'Y-m-d H:i:s'); - } - else { - $row['last_imported'] = ''; - } + return $row; } diff --git a/src/Form/MigrationExecuteForm.php b/src/Form/MigrationExecuteForm.php new file mode 100644 index 0000000..67cd3b0 --- /dev/null +++ b/src/Form/MigrationExecuteForm.php @@ -0,0 +1,229 @@ +migrationPluginManager = $migration_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.migration') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'migration_execute_form'; + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + + $form = []; + + $form['operations'] = $this->migrateMigrateOperations(); + + return $form; + } + + + /** + * Get Operations. + */ + private function migrateMigrateOperations() { + // Build the 'Update options' form. + $form = [ + '#type' => 'fieldset', + '#title' => t('Operations'), + ]; + $options = [ + 'import' => t('Import'), + 'rollback' => t('Rollback'), + 'stop' => t('Stop'), + 'reset' => t('Reset'), + ]; + $form['operation'] = [ + '#type' => 'select', + '#title' => t('Choose an operation to run'), + '#options' => $options, + '#default_value' => 'import', + '#required' => TRUE, + ]; + $form['submit'] = [ + '#type' => 'submit', + '#value' => t('Execute'), + ]; + $definitions = []; + $definitions[] = $this->t('Import: Imports all previously unprocessed records from the source, plus any records marked for update, into destination Drupal objects.'); + $definitions[] = $this->t('Rollback: Deletes all Drupal objects created by the import.'); + $definitions[] = $this->t('Stop: Cleanly interrupts any import or rollback processes that may currently be running.'); + $definitions[] = $this->t('Reset: Sometimes a process may fail to stop cleanly, and be left stuck in an Importing or Rolling Back status. Choose Reset to clear the status and permit other operations to proceed.'); + $form['definitions'] = [ + '#theme' => 'item_list', + '#title' => $this->t('Definitions'), + '#list_type' => 'ul', + '#items' => $definitions, + ]; + + $form['options'] = [ + '#type' => 'fieldset', + '#title' => t('Options'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ]; + $form['options']['update'] = [ + '#type' => 'checkbox', + '#title' => t('Update'), + '#description' => t('Check this box to update all previously-imported content + in addition to importing new content. Leave unchecked to only import + new content'), + ]; + $form['options']['force'] = [ + '#type' => 'checkbox', + '#title' => t('Ignore dependencies'), + '#description' => t('Check this box to ignore dependencies when running imports + - all tasks will run whether or not their dependent tasks have + completed.'), + ]; + $form['options']['limit'] = [ + '#type' => 'textfield', + '#title' => t('Limit to:'), + '#size' => 10, + '#description' => t('Set a limit of how many items to process for each migration task.'), + ]; + return $form; + } + + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + if (empty($form_state->getValue('operation'))) { + $form_state->setErrorByName('operation', $this->t('Please select an operation.')); + return; + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + + $operation = $form_state->getValue('operation'); + + if ($form_state->getValue('limit')) { + $limit = $form_state->getValue('limit'); + } + else { + $limit = 0; + } + + if ($form_state->getValue('update')) { + $update = $form_state->getValue('update'); + } + else { + $update = 0; + } + if ($form_state->getValue('force')) { + $force = $form_state->getValue('force'); + } + else { + $force = 0; + } + + $migration_name = \Drupal::routeMatch()->getParameter('migration'); + + if ($migration_name) { + + /** @var MigrationInterface $migration */ + $migration = $this->migrationPluginManager->createInstance($migration_name); + $migrateMessage = new MigrateMessage(); + + switch ($operation) { + case 'import': + + $options = [ + 'limit' => $limit, + 'update' => $update, + 'force' => $force, + ]; + + $executable = new MigrateBatchExecutable($migration, $migrateMessage, $options); + $executable->batchImport(); + + break; + + case 'rollback': + + $options = [ + 'limit' => $limit, + 'update' => $update, + 'force' => $force + ]; + + $executable = new MigrateBatchExecutable($migration, $migrateMessage, $options); + $executable->rollback(); + + break; + + case 'stop': + + $migration->interruptMigration(MigrationInterface::RESULT_STOPPED); + + break; + + case 'reset': + + $migration->setStatus(MigrationInterface::STATUS_IDLE); + + break; + + } + } + } + +} diff --git a/src/MigrateBatchExecutable.php b/src/MigrateBatchExecutable.php new file mode 100644 index 0000000..d1aa35e --- /dev/null +++ b/src/MigrateBatchExecutable.php @@ -0,0 +1,290 @@ +updateExistingRows = $options['update']; + } + + if (isset($options['force'])) { + $this->checkDependencies = $options['force']; + } + + parent::__construct($migration, $message, $options); + $this->migrationPluginManager = \Drupal::getContainer()->get('plugin.manager.migration'); + } + + /** + * Sets the current batch content so listeners can update the messages. + * + * @param array $context + */ + public function setBatchContext(&$context) { + $this->batchContext = &$context; + } + + /** + * Gets a reference to the current batch context. + * + * @return array + */ + public function &getBatchContext() { + return $this->batchContext; + } + + /** + * Setup batch operations for running the migration. + */ + public function batchImport() { + // Create the batch operations for each migration that needs to be executed. + // This includes the migration for this executable, but also the dependent + // migrations. + $operations = $this->batchOperations([$this->migration], 'import', [ + 'limit' => $this->itemLimit, + 'update' => $this->updateExistingRows, + 'force' => $this->checkDependencies + ]); + + if (count($operations) > 0) { + $batch = [ + 'operations' => $operations, + 'title' => t('Migrating %migrate', ['%migrate' => $this->migration->label()]), + 'init_message' => t('Start migrating %migrate', ['%migrate' => $this->migration->label()]), + 'progress_message' => t('Migrating %migrate', ['%migrate' => $this->migration->label()]), + 'error_message' => t('An error occurred while migrating %migrate.', ['%migrate' => $this->migration->label()]), + 'finished' => '\Drupal\migrate_tools\MigrateBatchExecutable::batchFinishedImport', + ]; + + batch_set($batch); + } + } + + /** + * Helper to generate the batch operations for importing migrations. + * + * @param array $migrations + * @param array $operation + * @param array $options + * + * @return array + */ + protected function batchOperations($migrations, $operation, $options = []) { + + $operations = []; + + /** + * @var string $id + * @var Migration $migration + */ + foreach ($migrations as $id => $migration) { + + if (!empty($options['update'])) { + $migration->getIdMap()->prepareUpdate(); + } + + if (!empty($options['force'])) { + $migration->set('requirements', []); + } + else { + $dependencies = $migration->getMigrationDependencies(); + if (!empty($dependencies['required'])) { + $required_migrations = $this->migrationPluginManager->createInstances($dependencies['required']); + // For dependent migrations will need to be migrate all items. + $dependent_options = $options; + $dependent_options['limit'] = 0; + $operations += $this->batchOperations($required_migrations, $operation, [ + 'limit' => 0, + 'update' => $options['update'], + 'force' => $options['force'] + ]); + } + } + + $operations[] = [ + '\Drupal\migrate_tools\MigrateBatchExecutable::batchProcessImport', + [$migration->id(), $options] + ]; + } + + return $operations; + } + + /** + * Batch 'operation' callback + * + * @param string $migration_id + * @param array $options + * @param array $context + * + */ + static public function batchProcessImport($migration_id, $options, &$context) { + + if (empty($context['sandbox'])) { + $context['finished'] = 0; + $context['sandbox'] = []; + $context['sandbox']['total'] = 0; + $context['sandbox']['counter'] = 0; + $context['sandbox']['batch_limit'] = 0; + $context['sandbox']['operation'] = MigrateBatchExecutable::BATCH_IMPORT; + } + + // Prepare the migration executable. + $message = new MigrateMessage(); + /** @var MigrationInterface $migration */ + $migration = \Drupal::getContainer()->get('plugin.manager.migration')->createInstance($migration_id); + $executable = new MigrateBatchExecutable($migration, $message, $options); + + if (empty($context['sandbox']['total'])) { + $context['sandbox']['total'] = $executable->getSource()->count(); + $context['sandbox']['batch_limit'] = $executable->calculateBatchLimit($context); + $context['results'][$migration->id()] = [ + '@numitems' => 0, + '@created' => 0, + '@updated' => 0, + '@failures' => 0, + '@ignored' => 0, + '@name' => $migration->id() + ]; + } + + // Every iteration, we reset out batch counter. + $context['sandbox']['batch_counter'] = 0; + + // Make sure we know our batch context. + $executable->setBatchContext($context); + + // Do the import. + $result = $executable->import(); + + // Store the result, we will need to combine the results of all our iterations. + $context['results'][$migration->id()] = [ + '@numitems' => $context['results'][$migration->id()]['@numitems'] + $executable->getProcessedCount(), + '@created' => $context['results'][$migration->id()]['@created'] + $executable->getCreatedCount(), + '@updated' => $context['results'][$migration->id()]['@updated'] + $executable->getUpdatedCount(), + '@failures' => $context['results'][$migration->id()]['@failures'] + $executable->getFailedCount(), + '@ignored' => $context['results'][$migration->id()]['@ignored'] + $executable->getIgnoredCount(), + '@name' => $migration->id() + ]; + + // Do some housekeeping. + if ( + $result != MigrationInterface::RESULT_INCOMPLETE + ) { + $context['finished'] = 1; + } + else { + $context['sandbox']['counter'] = $context['results'][$migration->id()]['@numitems']; + if ($context['sandbox']['counter'] <= $context['sandbox']['total']) { + $context['finished'] = ((float) $context['sandbox']['counter'] / (float) $context['sandbox']['total']); + $context['message'] = t('Importing %migration (@percent%).', [ + '%migration' => $migration->label(), + '@percent' => (int) ($context['finished'] * 100) + ]); + } + } + + } + + /** + * Finished callback for import batches. + * + * @param $success + * @param $results + * @param $operations + * @param $elapsed + */ + static public function batchFinishedImport($success, $results, $operations, $elapsed) { + if ($success) { + foreach ($results as $migration_id => $result) { + $singular_message = "Processed 1 item (@created created, @updated updated, @failures failed, @ignored ignored) - done with '@name'"; + $plural_message = "Processed @numitems items (@created created, @updated updated, @failures failed, @ignored ignored) - done with '@name'"; + drupal_set_message(\Drupal::translation()->formatPlural($result['@numitems'], + $singular_message, + $plural_message, + $result)); + } + } + } + + /** + * @inheritdoc + */ + public function checkStatus() { + $status = parent::checkStatus(); + + if ($status == MigrationInterface::RESULT_COMPLETED) { + // Do some batch housekeeping. + $context = $this->getBatchContext(); + + if (!empty($context['sandbox']) && $context['sandbox']['operation'] == MigrateBatchExecutable::BATCH_IMPORT) { + $context['sandbox']['batch_counter']++; + if ($context['sandbox']['batch_counter'] >= $context['sandbox']['batch_limit']) { + $status = MigrationInterface::RESULT_INCOMPLETE; + } + } + } + + return $status; + } + + /** + * Calculates how much a single batch iteration will handle. + * + * @param $context + * + * @return float + */ + public function calculateBatchLimit($context) { + // TODO Maybe we need some other more sophisticated logic here? + return ceil($context['sandbox']['total'] / 100); + } + +} diff --git a/src/MigrateExecutable.php b/src/MigrateExecutable.php index 84eb0ad..01dba42 100644 --- a/src/MigrateExecutable.php +++ b/src/MigrateExecutable.php @@ -93,10 +93,12 @@ public function __construct(MigrationInterface $migration, MigrateMessageInterfa $this->feedback = $options['feedback']; } if (isset($options['idlist'])) { - $this->idlist = explode(',', $options['idlist']); - array_walk($this->idlist, function(&$value, $key) { - $value = explode(':', $value); - }); + if (is_string($options['idlist'])) { + $this->idlist = explode(',', $options['idlist']); + array_walk($this->idlist, function (&$value, $key) { + $value = explode(':', $value); + }); + } } $this->listeners[MigrateEvents::MAP_SAVE] = [$this, 'onMapSave']; diff --git a/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.fruit_terms.yml b/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.fruit_terms.yml new file mode 100755 index 0000000..7f606d1 --- /dev/null +++ b/tests/modules/migrate_tools_test/config/install/migrate_plus.migration.fruit_terms.yml @@ -0,0 +1,32 @@ +langcode: en +status: true +dependencies: { } +id: fruit_terms +label: Fruit Terms +class: null +field_plugin_method: null +cck_plugin_method: null +migration_tags: { } +migration_group: default +source: + plugin: embedded_data + data_rows: + - + name: Apple + - + name: Banana + - + name: Orange + ids: + name: + type: string + constants: + vocabulary: fruit +process: + name: name + vid: constants/vocabulary +destination: + plugin: entity:taxonomy_term +migration_dependencies: + required: { } + optional: { } diff --git a/tests/modules/migrate_tools_test/config/install/migrate_plus.migration_group.default.yml b/tests/modules/migrate_tools_test/config/install/migrate_plus.migration_group.default.yml new file mode 100644 index 0000000..0f35a21 --- /dev/null +++ b/tests/modules/migrate_tools_test/config/install/migrate_plus.migration_group.default.yml @@ -0,0 +1,9 @@ +langcode: en +status: true +dependencies: { } +id: default +label: Default +description: '' +source_type: '' +module: null +shared_configuration: null diff --git a/tests/modules/migrate_tools_test/migrate_tools_test.info.yml b/tests/modules/migrate_tools_test/migrate_tools_test.info.yml new file mode 100644 index 0000000..a332432 --- /dev/null +++ b/tests/modules/migrate_tools_test/migrate_tools_test.info.yml @@ -0,0 +1,8 @@ +type: module +name: Migrate Tools Test +description: 'Test module to test Migrate Tools.' +package: Testing +core: 8.x +dependencies: + - drupal:migrate (>=8.3) + - migrate_plus:migrate_plus diff --git a/tests/src/Functional/MigrateExecutionFormTest.php b/tests/src/Functional/MigrateExecutionFormTest.php new file mode 100644 index 0000000..76bb7ca --- /dev/null +++ b/tests/src/Functional/MigrateExecutionFormTest.php @@ -0,0 +1,132 @@ +vocabulary = $this->createVocabulary(['vid' => 'fruit', 'name' => 'Fruit']); + $this->vocabularyQuery = $this->container->get('entity_type.manager') + ->getStorage('taxonomy_term') + ->getQuery(); + // Log in as user 1. Migrations in the UI can only be performed as user 1. + $this->drupalLogin($this->rootUser); + } + + /** + * Tests execution of import and rollback of a migration. + * + * @throws \Behat\Mink\Exception\ExpectationException + */ + public function testExecution() { + $group = 'default'; + $migration = 'fruit_terms'; + $urlPath = "/admin/structure/migrate/manage/{$group}/migrations/{$migration}/execute"; + $real_count = $this->vocabularyQuery->count()->execute(); + $expected_count = 0; + $this->assertEquals($expected_count, $real_count); + $this->drupalGet($urlPath); + $this->assertSession()->responseContains('Choose an operation to run'); + $edit = [ + 'operation' => 'import', + ]; + $this->drupalPostForm($urlPath, $edit, t('Execute')); + $real_count = $this->vocabularyQuery->count()->execute(); + $expected_count = 3; + $this->assertEquals($expected_count, $real_count); + $edit = [ + 'operation' => 'rollback', + ]; + $this->drupalPostForm($urlPath, $edit, t('Execute')); + $real_count = $this->vocabularyQuery->count()->execute(); + $expected_count = 0; + $this->assertEquals($expected_count, $real_count); + $edit = [ + 'operation' => 'import', + ]; + $this->drupalPostForm($urlPath, $edit, t('Execute')); + $real_count = $this->vocabularyQuery->count()->execute(); + $expected_count = 3; + $this->assertEquals($expected_count, $real_count); + } + + /** + * Creates a custom vocabulary based on default settings. + * + * @param array $values + * An array of settings to change from the defaults. + * Example: 'vid' => 'foo'. + * + * @return \Drupal\taxonomy\VocabularyInterface + * Created vocabulary. + */ + protected function createVocabulary(array $values = []) { + // Find a non-existent random vocabulary name. + if (!isset($values['vid'])) { + do { + $id = strtolower($this->randomMachineName(8)); + } while (Vocabulary::load($id)); + } + else { + $id = $values['vid']; + } + $values += [ + 'id' => $id, + 'name' => $id, + ]; + $vocabulary = Vocabulary::create($values); + $status = $vocabulary->save(); + + $this->assertSame($status, SAVED_NEW); + + return $vocabulary; + } + +}