first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
name: Test display of migrate message
type: module
description: Tests the display of migrate messages.
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,13 @@
id: custom_test
label: Test display of upgrade messages
source:
plugin: embedded_data
data_rows:
- id: 0
ids:
id:
type: integer
process:
id: id
destination:
plugin: null

View File

@@ -0,0 +1,9 @@
id: custom_test_db
label: Test display of upgrade messages
source:
plugin: extension
name: i18n_menu
process:
id: id
destination:
plugin: null

View File

@@ -0,0 +1,10 @@
name: Cacheable Embedded Data Test
type: module
description: Module containing a cacheable embedded data source.
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,33 @@
<?php
namespace Drupal\migrate_cache_counts_test\Plugin\migrate\source;
use Drupal\migrate\Plugin\migrate\source\EmbeddedDataSource;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
/**
* A copy of embedded_data which allows caching the count.
*
* @MigrateSource(
* id = "cacheable_embedded_data",
* source_module = "migrate"
* )
*/
class CacheableEmbeddedDataSource extends EmbeddedDataSource {
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function count($refresh = FALSE) {
return SourcePluginBase::count($refresh);
}
/**
* {@inheritdoc}
*/
protected function doCount() {
return parent::count(TRUE);
}
}

View File

@@ -0,0 +1,10 @@
name: 'Migrate entity test'
type: module
description: 'Support module for entity destination test.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,36 @@
<?php
namespace Drupal\migrate_entity_test\Entity;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
/**
* Defines a content entity type that has a string ID.
*
* @ContentEntityType(
* id = "migrate_string_id_entity_test",
* label = @Translation("String id entity test"),
* base_table = "migrate_entity_test_string_id",
* entity_keys = {
* "id" = "id",
* }
* )
*/
class StringIdEntityTest extends ContentEntityBase {
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
return [
'id' => BaseFieldDefinition::create('integer')
->setSetting('size', 'big')
->setLabel('ID'),
'version' => BaseFieldDefinition::create('string')
->setLabel('Version'),
];
}
}

View File

@@ -0,0 +1,9 @@
name: 'Migrate events test'
type: module
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,40 @@
<?php
namespace Drupal\migrate_events_test\Plugin\migrate\destination;
use Drupal\migrate\Attribute\MigrateDestination;
use Drupal\migrate\Plugin\migrate\destination\DestinationBase;
use Drupal\migrate\Row;
/**
* Migration dummy destination.
*/
#[MigrateDestination(
id: 'dummy',
requirements_met: TRUE
)]
class DummyDestination extends DestinationBase {
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['value']['type'] = 'string';
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields() {
return ['value' => 'Dummy value'];
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []) {
return ['value' => $row->getDestinationProperty('value')];
}
}

View File

@@ -0,0 +1,12 @@
name: 'Migration Expected Migrations Test'
type: module
description: 'Provides test migrations to test getMigrationDependencies.'
package: Testing
# version: VERSION
dependencies:
- drupal:migrate
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,7 @@
id: m1
label: "Sample Migration 1"
source:
plugin: empty
process: {}
destination:
plugin: 'null'

View File

@@ -0,0 +1,7 @@
id: m2
label: "Sample Migration 2"
source:
plugin: empty
process: {}
destination:
plugin: 'null'

View File

@@ -0,0 +1,7 @@
id: m3
label: "Sample Migration 3"
source:
plugin: empty
process: {}
destination:
plugin: 'null'

View File

@@ -0,0 +1,7 @@
id: m4
label: "Sample Migration 4"
source:
plugin: empty
process: {}
destination:
plugin: 'null'

View File

@@ -0,0 +1,7 @@
id: m5
label: "Sample Migration 5"
source:
plugin: empty
process: {}
destination:
plugin: 'null'

View File

@@ -0,0 +1,12 @@
name: 'Migration external translated test'
type: module
package: Testing
# version: VERSION
dependencies:
- drupal:node
- drupal:migrate
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,19 @@
id: external_translated_test_node
label: External translated content
source:
plugin: migrate_external_translated_test
default_lang: true
constants:
type: external_test
process:
type: constants/type
title: title
langcode:
plugin: static_map
source: lang
map:
English: en
French: fr
Spanish: es
destination:
plugin: entity:node

View File

@@ -0,0 +1,27 @@
id: external_translated_test_node_translation
label: External translated content translations
source:
plugin: migrate_external_translated_test
default_lang: false
constants:
type: external_test
process:
nid:
plugin: migration_lookup
source: name
migration: external_translated_test_node
type: constants/type
title: title
langcode:
plugin: static_map
source: lang
map:
English: en
French: fr
Spanish: es
destination:
plugin: entity:node
translations: true
migration_dependencies:
required:
- external_translated_test_node

View File

@@ -0,0 +1,78 @@
<?php
namespace Drupal\migrate_external_translated_test\Plugin\migrate\source;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
/**
* A simple migrate source for our tests.
*
* @MigrateSource(
* id = "migrate_external_translated_test",
* source_module = "migrate_external_translated_test"
* )
*/
class MigrateExternalTranslatedTestSource extends SourcePluginBase {
/**
* The data to import.
*
* @var array
*/
protected $import = [
['name' => 'cat', 'title' => 'Cat', 'lang' => 'English'],
['name' => 'cat', 'title' => 'Chat', 'lang' => 'French'],
['name' => 'cat', 'title' => 'es - Cat', 'lang' => 'Spanish'],
['name' => 'dog', 'title' => 'Dog', 'lang' => 'English'],
['name' => 'dog', 'title' => 'fr - Dog', 'lang' => 'French'],
['name' => 'monkey', 'title' => 'Monkey', 'lang' => 'English'],
];
/**
* {@inheritdoc}
*/
public function fields() {
return [
'name' => $this->t('Unique name'),
'title' => $this->t('Title'),
'lang' => $this->t('Language'),
];
}
/**
* {@inheritdoc}
*/
public function __toString() {
return '';
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['name']['type'] = 'string';
if (!$this->configuration['default_lang']) {
$ids['lang']['type'] = 'string';
}
return $ids;
}
/**
* {@inheritdoc}
*/
protected function initializeIterator() {
$data = [];
// Keep the rows with the right languages.
$want_default = $this->configuration['default_lang'];
foreach ($this->import as $row) {
$is_english = $row['lang'] == 'English';
if ($want_default == $is_english) {
$data[] = $row;
}
}
return new \ArrayIterator($data);
}
}

View File

@@ -0,0 +1,11 @@
type: module
name: Migration High Water Test
description: 'Provides test fixtures for testing high water marks.'
package: Testing
dependencies:
- drupal:migrate
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,16 @@
id: high_water_test
label: High water test.
source:
plugin: high_water_test
high_water_property:
name: changed
destination:
plugin: entity:node
migration_tags:
test: test
process:
changed: changed
title: title
type:
plugin: default_value
default_value: high_water_import_node

View File

@@ -0,0 +1,54 @@
<?php
namespace Drupal\migrate_high_water_test\Plugin\migrate\source;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
/**
* Source plugin for migration high water tests.
*
* @MigrateSource(
* id = "high_water_test"
* )
*/
class HighWaterTest extends SqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$field_names = array_keys($this->fields());
$query = $this
->select('high_water_node', 'm')
->fields('m', $field_names);
foreach ($field_names as $field_name) {
$query->groupBy($field_name);
}
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'id' => $this->t('Id'),
'title' => $this->t('Title'),
'changed' => $this->t('Changed'),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'id' => [
'type' => 'integer',
],
];
}
}

View File

@@ -0,0 +1,12 @@
name: 'Migration Lookup Test'
type: module
description: 'Provides test migrations to test migration lookup service.'
package: Testing
# version: VERSION
dependencies:
- drupal:migrate
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,20 @@
id: sample_lookup_migration
label: Sample Lookup Migration
source:
plugin: embedded_data
data_rows:
- id: 17
nid: 1
title: Node 1
- id: 25
nid: 2
title: Node 2
ids:
id:
type: integer
process:
nid: nid
title: title
destination:
default_bundle: node_lookup
plugin: entity:node

View File

@@ -0,0 +1,20 @@
id: sample_lookup_migration_2
label: Sample Lookup Migration
source:
plugin: embedded_data
data_rows:
- id: 27
nid: 3
title: Node 3
- id: 35
nid: 4
title: Node 4
ids:
id:
type: integer
process:
nid: nid
title: title
destination:
default_bundle: node_lookup
plugin: entity:node

View File

@@ -0,0 +1,28 @@
id: sample_lookup_migration_multiple_source_ids
label: "Sample Lookup Migration With Multiple Source Ids."
source:
plugin: embedded_data
data_rows:
- id: 17
version_id: 17
nid: 1
title: "Node 1"
- id: 25
version_id: 25
nid: 2
title: "Node 2"
- id: 25
version_id: 26
nid: 3
title: "Node 3"
ids:
id:
type: integer
version_id:
type: integer
process:
nid: nid
title: title
destination:
default_bundle: node_lookup
plugin: entity:node

View File

@@ -0,0 +1,20 @@
id: sample_lookup_migration_string_ids
label: Sample Lookup Migration with string ids
source:
plugin: embedded_data
data_rows:
- id: node1
nid: 10
title: Node 10
- id: node2
nid: 11
title: Node 11
ids:
id:
type: string
process:
nid: nid
title: title
destination:
default_bundle: node_lookup
plugin: entity:node

View File

@@ -0,0 +1,11 @@
name: 'Migration missing database test'
type: module
package: Testing
# version: VERSION
dependencies:
- drupal:migrate
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,8 @@
id: missing_database
label: Migration using a SQL source
source:
plugin: migrate_missing_database_test
process: {}
destination:
plugin: 'null'
migration_dependencies: {}

View File

@@ -0,0 +1,52 @@
<?php
namespace Drupal\migrate_missing_database_test\Plugin\migrate\source;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
/**
* A simple migrate source for the missing database tests.
*
* @MigrateSource(
* id = "migrate_missing_database_test",
* source_module = "migrate_missing_database_test",
* requirements_met = true
* )
*/
class MigrateMissingDatabaseSource extends SqlBase {
/**
* {@inheritdoc}
*/
public function query(): SelectInterface {
$field_names = ['id'];
$query = $this
->select('missing_database', 'm')
->fields('m', $field_names);
return $query;
}
/**
* {@inheritdoc}
*/
public function fields(): array {
$fields = [
'id' => $this->t('ID'),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIds(): array {
return [
'id' => [
'type' => 'integer',
],
];
}
}

View File

@@ -0,0 +1,13 @@
name: 'Migrate No Migrate Drupal Test'
type: module
description: Provides fixture for testing without migrate_drupal.
package: Testing
# version: VERSION
dependencies:
- drupal:migrate
- drupal:node
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,7 @@
migrate_no_migrate_drupal_test.execute:
path: '/migrate_no_migrate_drupal_test/execute'
defaults:
_controller: '\Drupal\migrate_no_migrate_drupal_test\Controller\ExecuteMigration::execute'
_title: 'Execute'
requirements:
_access: 'TRUE'

View File

@@ -0,0 +1,22 @@
id: node_migration_no_migrate_drupal
label: Node Migration No Migrate Drupal
source:
plugin: embedded_data
data_rows:
-
id: 1
nid: 1
title: Node 1
-
id: 2
nid: 2
title: Node 2
ids:
id:
type: integer
process:
nid: nid
title: title
destination:
default_bundle: no_migrate_drupal
plugin: entity:node

View File

@@ -0,0 +1,45 @@
<?php
namespace Drupal\migrate_no_migrate_drupal_test\Controller;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Core\Controller\ControllerBase;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Custom controller to execute the test migrations.
*
* This controller class is required for the proper functional testing of
* migration dependencies. Otherwise, the migration directly executed from the
* functional test would use the functional test's class map and autoloader. The
* functional test has all the classes available to it but the controller
* does not.
*/
class ExecuteMigration extends ControllerBase {
/**
* Run the node_migration_no_migrate_drupal test migration.
*
* @return array
* A renderable array.
*/
public function execute() {
$migration_plugin_manager = \Drupal::service('plugin.manager.migration');
$definitions = $migration_plugin_manager->getDefinitions();
if ($definitions['node_migration_no_migrate_drupal']['label'] !== 'Node Migration No Migrate Drupal') {
throw new InvalidPluginDefinitionException('node_migration_no_migrate_drupal');
}
$migrations = $migration_plugin_manager->createInstances('');
$result = (new MigrateExecutable($migrations['node_migration_no_migrate_drupal']))->import();
if ($result !== MigrationInterface::RESULT_COMPLETED) {
throw new \RuntimeException('Migration failed');
}
return [
'#type' => 'markup',
'#markup' => 'Migration was successful.',
];
}
}

View File

@@ -0,0 +1,10 @@
name: 'Migrate module prepareRow tests'
type: module
description: 'Support module for source plugin prepareRow testing.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,27 @@
<?php
/**
* @file
* Tests the migration source plugin prepareRow() exception handling.
*/
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Row;
/**
* Implements hook_migrate_prepare_row().
*/
function migrate_prepare_row_test_migrate_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) {
// Test both options for save_to_map.
$data = $row->getSourceProperty('data');
if ($data == 'skip_and_record') {
// Record mapping but don't record a message.
throw new MigrateSkipRowException('', TRUE);
}
elseif ($data == 'skip_and_do_not_record') {
// Don't record mapping but record a message.
throw new MigrateSkipRowException('skip_and_do_not_record message', FALSE);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Drupal\migrate_prepare_row_test\Plugin\migrate\process;
use Drupal\migrate\Attribute\MigrateProcess;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
/**
* Provides a testing process plugin that skips rows.
*/
#[MigrateProcess('test_skip_row_process')]
class TestSkipRowProcess extends ProcessPluginBase {
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
// Test both options for save_to_map.
$data = $row->getSourceProperty('data');
if ($data == 'skip_and_record (use plugin)') {
throw new MigrateSkipRowException('', TRUE);
}
elseif ($data == 'skip_and_do_not_record (use plugin)') {
throw new MigrateSkipRowException('', FALSE);
}
return $value;
}
}

View File

@@ -0,0 +1,11 @@
type: module
name: Migrate query batch Source test
description: 'Provides a database table and records for SQL import with batch testing.'
package: Testing
dependencies:
- drupal:migrate
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,45 @@
<?php
namespace Drupal\migrate_query_batch_test\Plugin\migrate\source;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
/**
* Source plugin for migration high water tests.
*
* @MigrateSource(
* id = "query_batch_test"
* )
*/
class QueryBatchTest extends SqlBase {
/**
* {@inheritdoc}
*/
public function query() {
return ($this->select('query_batch_test', 'q')->fields('q'));
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'id' => $this->t('Id'),
'data' => $this->t('data'),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'id' => [
'type' => 'integer',
],
];
}
}

View File

@@ -0,0 +1,10 @@
name: 'Migrate module prepareRow tests'
type: module
description: 'Support module for source plugin prepareRow testing.'
package: Testing
# version: VERSION
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,20 @@
<?php
/**
* @file
* Tests the migration source plugin prepareRow() exception.
*/
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateSkipRowException;
use Drupal\migrate\Plugin\MigrateSourceInterface;
use Drupal\migrate\Row;
/**
* Implements hook_migrate_prepare_row().
*/
function migrate_skip_all_rows_test_migrate_prepare_row(Row $row, MigrateSourceInterface $source, MigrationInterface $migration) {
if (\Drupal::state()->get('migrate_skip_all_rows_test_migrate_prepare_row')) {
throw new MigrateSkipRowException();
}
}

View File

@@ -0,0 +1,9 @@
type: module
name: Migrate SqlBase count cache test
description: Provides a source plugin to test that counts are cached in SQL sources.
package: Testing
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,43 @@
<?php
namespace Drupal\migrate_sql_count_cache_test\Plugin\migrate\source;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
/**
* Source plugin for Sql count cache test.
*
* @MigrateSource(
* id = "sql_count_cache"
* )
*/
class SqlCountCache extends SqlBase {
/**
* {@inheritdoc}
*/
public function fields() {
return [
'id' => $this->t('Id'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'id' => [
'type' => 'integer',
],
];
}
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('source_table', 's')->fields('s', ['id']);
}
}

View File

@@ -0,0 +1,12 @@
name: 'Migration Stub Test'
type: module
description: 'Provides test migrations to test migration stub service.'
package: Testing
# version: VERSION
dependencies:
- drupal:migrate
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,25 @@
id: sample_stubbing_migration
label: "Sample Stubbing Migration"
source:
plugin: embedded_data
data_rows:
- id: 17
title: "Sample 1"
body_value: "This is the body for ID 17"
body_format: "plain_text"
- id: 25
title: "Sample 2"
body_value: "This is the body for ID 25"
body_format: "plain_text"
- id: 33
title: "Sample 3"
ids:
id:
type: integer
process:
title: title
body/0/value: body_value
body/0/format: body_format
destination:
default_bundle: node_stub
plugin: entity:node

View File

@@ -0,0 +1,27 @@
id: sample_stubbing_migration_with_multiple_source_ids
label: "Sample stubbing migration with multiple source ids."
source:
plugin: embedded_data
data_rows:
- id: 17
version_id: 17
title: "Sample 1"
body_value: "This is the body for ID 17"
body_format: "plain_text"
- id: 25
version_id: 25
title: "Sample 2"
body_value: "This is the body for ID 25"
body_format: "plain_text"
ids:
id:
type: integer
version_id:
type: integer
process:
title: title
body/0/value: body_value
body/0/format: body_format
destination:
default_bundle: node_stub
plugin: entity:node

View File

@@ -0,0 +1,12 @@
name: Migration Tag Test
type: module
description: Provide migrations for testing 'migration_tags' property
package: Testing
# version: VERSION
dependencies:
- drupal:migrate
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,16 @@
id: tag_test_0
label: migration tag test 0
migration_tags:
- test
- tag_test_0
source:
plugin: embedded_data
data_rows:
- id: 0
ids:
id:
type: integer
process:
id: id
destination:
plugin: null

View File

@@ -0,0 +1,16 @@
id: tag_test_1
label: migration tag test 1
migration_tags:
- test
- tag_test_1
source:
plugin: embedded_data
data_rows:
- id: 0
ids:
id:
type: integer
process:
id: id
destination:
plugin: null

View File

@@ -0,0 +1,13 @@
id: tag_test_no_tag
label: migration tag test no tags
source:
plugin: embedded_data
data_rows:
- id: 0
ids:
id:
type: integer
process:
id: id
destination:
plugin: null

View File

@@ -0,0 +1,11 @@
type: module
name: Migration Track Changes Test
description: 'Provides test fixtures for testing track changes marks.'
package: Testing
dependencies:
- drupal:migrate
# Information added by Drupal.org packaging script on 2024-07-04
version: '10.3.1'
project: 'drupal'
datestamp: 1720094222

View File

@@ -0,0 +1,19 @@
id: track_changes_test
label: Track changes test.
source:
plugin: track_changes_test
track_changes: true
batch_size: 1
destination:
plugin: entity:taxonomy_term
migration_tags:
test: test
process:
name: name
vid:
plugin: default_value
default_value: track_changes_import_term
'description/value': description
'description/format':
plugin: default_value
default_value: 'basic_html'

View File

@@ -0,0 +1,54 @@
<?php
namespace Drupal\migrate_track_changes_test\Plugin\migrate\source;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
/**
* Source plugin for migration track changes tests.
*
* @MigrateSource(
* id = "track_changes_test"
* )
*/
class TrackChangesTest extends SqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$field_names = array_keys($this->fields());
$query = $this
->select('track_changes_term', 't')
->fields('t', $field_names);
foreach ($field_names as $field_name) {
$query->groupBy($field_name);
}
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
$fields = [
'tid' => $this->t('Term id'),
'name' => $this->t('Name'),
'description' => $this->t('Description'),
];
return $fields;
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [
'tid' => [
'type' => 'integer',
],
];
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Functional;
use Drupal\Tests\system\Functional\Module\GenericModuleTestBase;
/**
* Generic module test for migrate.
*
* @group migrate
*/
class GenericTest extends GenericModuleTestBase {}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Functional;
/**
* Tests for the MigrateController class.
*
* @group migrate
*/
class MigrateMessageControllerTest extends MigrateMessageTestBase {
/**
* Tests the overview page for migrate messages.
*
* Tests the overview page with the following scenarios;
* - No message tables.
* - With message tables.
*/
public function testOverview(): void {
$session = $this->assertSession();
// First, test with no source database or message tables.
$this->drupalGet('/admin/reports/migration-messages');
$session->titleEquals('Migration messages | Drupal');
$session->pageTextContainsOnce('There are no migration message tables.');
// Create map and message tables.
$this->createTables($this->migrationIds);
// Now, test with message tables.
$this->drupalGet('/admin/reports/migration-messages');
foreach ($this->migrationIds as $migration_id) {
$session->pageTextContains($migration_id);
}
}
/**
* Tests the detail pages for migrate messages.
*
* Tests the detail page with the following scenarios;
* - No source database connection or message tables with a valid and an
* invalid migration.
* - A source database connection with message tables with a valid and an
* invalid migration.
* - A source database connection with message tables and a source plugin
* that does not have a description for a source ID in the values returned
* from fields().
*/
public function testDetail(): void {
$session = $this->assertSession();
// Details page with invalid migration.
$this->drupalGet('/admin/reports/migration-messages/invalid');
$session->statusCodeEquals(404);
// Details page with valid migration.
$this->drupalGet('/admin/reports/migration-messages/custom_test');
$session->statusCodeEquals(404);
// Create map and message tables.
$this->createTables($this->migrationIds);
$not_available_text = "When there is an error processing a row, the migration system saves the error message but not the source ID(s) of the row. That is why some messages in this table have 'Not available' in the source ID column(s).";
// Test details page for each migration.
foreach ($this->migrationIds as $migration_id) {
$this->drupalGet("/admin/reports/migration-messages/$migration_id");
$session->pageTextContains($migration_id);
if ($migration_id == 'custom_test') {
$session->pageTextContains('Not available');
$session->pageTextContains($not_available_text);
}
}
// Details page with invalid migration.
$this->drupalGet('/admin/reports/migration-messages/invalid');
$session->statusCodeEquals(404);
// Details page for a migration without a map table.
$this->database->schema()->dropTable('migrate_map_custom_test');
$this->drupalGet('/admin/reports/migration-messages/custom_test');
$session->statusCodeEquals(404);
// Details page for a migration with a map table but no message table.
$this->createTables($this->migrationIds);
$this->database->schema()->dropTable('migrate_message_custom_test');
$this->drupalGet('/admin/reports/migration-messages/custom_test');
$session->pageTextContains('The message table is missing for this migration.');
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Functional;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests for the MessageForm class.
*
* @group migrate
*/
class MigrateMessageFormTest extends MigrateMessageTestBase {
/**
* Tests the message form.
*/
public function testFilter(): void {
$session = $this->assertSession();
// Create map and message tables.
$this->createTables($this->migrationIds);
// Expected counts for each error level.
$expected = [
MigrationInterface::MESSAGE_ERROR => 3,
MigrationInterface::MESSAGE_WARNING => 0,
MigrationInterface::MESSAGE_NOTICE => 0,
MigrationInterface::MESSAGE_INFORMATIONAL => 1,
];
// Confirm that all the entries are displayed.
$this->drupalGet('/admin/reports/migration-messages/custom_test');
$session->statusCodeEquals(200);
$messages = $this->getMessages();
$this->assertCount(4, $messages);
// Set the filter to match each of the two filter-type attributes and
// confirm the correct number of entries are displayed.
foreach ($expected as $level => $expected_count) {
$edit['severity[]'] = $level;
$this->submitForm($edit, 'Filter');
$count = $this->getLevelCounts($expected);
$this->assertEquals($expected_count, $count[$level], sprintf('Count for level %s failed', $level));
}
// Reset the filter
$this->submitForm([], 'Reset');
$messages = $this->getMessages();
$this->assertCount(4, $messages);
}
/**
* Gets the count of migration messages by level.
*
* @param array $levels
* The error levels to check.
*
* @return array
* The count of each error level keyed by the error level.
*/
protected function getLevelCounts(array $levels): array {
$entries = $this->getMessages();
$count = array_fill(1, count($levels), 0);
foreach ($entries as $entry) {
if (array_key_exists($entry['severity'], $levels)) {
$count[$entry['severity']]++;
}
}
return $count;
}
/**
* Gets the migrate messages.
*
* @return array[]
* List of log events where each event is an array with following keys:
* - msg_id: (string) A message id.
* - severity: (int) The MigrationInterface error level.
* - message: (string) The migration message.
*/
protected function getMessages(): array {
$levels = [
'Error' => MigrationInterface::MESSAGE_ERROR,
'Warning' => MigrationInterface::MESSAGE_WARNING,
'Notice' => MigrationInterface::MESSAGE_NOTICE,
'Info' => MigrationInterface::MESSAGE_INFORMATIONAL,
];
$entries = [];
$table = $this->xpath('.//table[@id="admin-migrate-msg"]/tbody/tr');
foreach ($table as $row) {
$cells = $row->findAll('css', 'td');
if (count($cells) === 3) {
$entries[] = [
'msg_id' => $cells[0]->getText(),
'severity' => $levels[$cells[1]->getText()],
'message' => $cells[2]->getText(),
];
}
}
return $entries;
}
}

View File

@@ -0,0 +1,239 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Functional;
use Drupal\Tests\BrowserTestBase;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
// cspell:ignore destid sourceid
/**
* Provides base class for testing migrate messages.
*
* @group migrate
*/
class MigrateMessageTestBase extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'message_test',
'migrate',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Migration IDs.
*/
protected $migrationIds = ['custom_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$user = $this->createUser(['view migration messages']);
$this->drupalLogin($user);
$this->database = \Drupal::database();
}
/**
* Creates map and message tables for testing.
*
* @see \Drupal\migrate\Plugin\migrate\id_map\Sql::ensureTables
*/
protected function createTables($migration_ids): void {
foreach ($migration_ids as $migration_id) {
$map_table_name = "migrate_map_$migration_id";
$message_table_name = "migrate_message_$migration_id";
if (!$this->database->schema()->tableExists($map_table_name)) {
$fields = [];
$fields['source_ids_hash'] = [
'type' => 'varchar',
'length' => '64',
'not null' => TRUE,
];
$fields['sourceid1'] = [
'type' => 'varchar',
'length' => '255',
'not null' => TRUE,
];
$fields['destid1'] = [
'type' => 'varchar',
'length' => '255',
'not null' => FALSE,
];
$fields['source_row_status'] = [
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => MigrateIdMapInterface::STATUS_IMPORTED,
];
$fields['rollback_action'] = [
'type' => 'int',
'size' => 'tiny',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => MigrateIdMapInterface::ROLLBACK_DELETE,
];
$fields['last_imported'] = [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 0,
];
$fields['hash'] = [
'type' => 'varchar',
'length' => '64',
'not null' => FALSE,
];
$schema = [
'description' => '',
'fields' => $fields,
'primary key' => ['source_ids_hash'],
];
$this->database->schema()->createTable($map_table_name, $schema);
$rows = [
[
'source_ids_hash' => '37c655d',
'sourceid1' => 'navigation',
'destid1' => 'tools',
'source_row_status' => '0',
'rollback_action' => '1',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => '3a34190',
'sourceid1' => 'menu-fixed-lang',
'destid1' => 'menu-fixed-lang',
'source_row_status' => '0',
'rollback_action' => '0',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => '3e51f67',
'sourceid1' => 'management',
'destid1' => 'admin',
'source_row_status' => '0',
'rollback_action' => '1',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => '94a5caa',
'sourceid1' => 'user-menu',
'destid1' => 'account',
'source_row_status' => '0',
'rollback_action' => '1',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => 'c0efbcca',
'sourceid1' => 'main-menu',
'destid1' => 'main',
'source_row_status' => '0',
'rollback_action' => '1',
'last_imported' => '0',
'hash' => '',
],
[
'source_ids_hash' => 'f64cb72f',
'sourceid1' => 'menu-test-menu',
'destid1' => 'menu-test-menu',
'source_row_status' => '0',
'rollback_action' => '0',
'last_imported' => '0',
'hash' => '',
],
];
foreach ($rows as $row) {
$this->database->insert($map_table_name)->fields($row)->execute();
}
}
if (!$this->database->schema()->tableExists($message_table_name)) {
$fields = [];
$fields['msgid'] = [
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
];
$fields['source_ids_hash'] = [
'type' => 'varchar',
'length' => '64',
'not null' => TRUE,
];
$fields['level'] = [
'type' => 'int',
'unsigned' => TRUE,
'not null' => TRUE,
'default' => 1,
];
$fields['message'] = [
'type' => 'text',
'size' => 'medium',
'not null' => TRUE,
];
$schema = [
'description' => '',
'fields' => $fields,
'primary key' => ['msgid'],
];
$this->database->schema()->createTable($message_table_name, $schema);
$rows = [
[
'msgid' => '1',
'source_ids_hash' => '28cfb3d1',
'level' => (string) MigrationInterface::MESSAGE_ERROR,
'message' => 'Config entities can not be stubbed.',
],
[
'msgid' => '2',
'source_ids_hash' => '28cfb3d1',
'level' => (string) MigrationInterface::MESSAGE_ERROR,
'message' => 'Config entities can not be stubbed.',
],
[
'msgid' => '3',
'source_ids_hash' => '05914d93',
'level' => (string) MigrationInterface::MESSAGE_ERROR,
'message' => 'Config entities can not be stubbed.',
],
[
'msgid' => '4',
'source_ids_hash' => '05914d93',
'level' => (string) MigrationInterface::MESSAGE_INFORMATIONAL,
'message' => 'Config entities can not be stubbed.',
],
];
foreach ($rows as $row) {
$this->database->insert($message_table_name)->fields($row)->execute();
}
}
}
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Functional;
use Drupal\node\Entity\Node;
use Drupal\Tests\BrowserTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Execute migration.
*
* This is intentionally a Functional test instead of a Kernel test because
* Kernel tests have proven to not catch all edge cases that are encountered
* via a Functional test.
*
* @group migrate
*/
class MigrateNoMigrateDrupalTest extends BrowserTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate',
'migrate_no_migrate_drupal_test',
'node',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createContentType(['type' => 'no_migrate_drupal']);
}
/**
* Tests execution of a migration.
*/
public function testExecutionNoMigrateDrupal(): void {
$this->drupalGet('/migrate_no_migrate_drupal_test/execute');
$this->assertSession()->pageTextContains('Migration was successful.');
$node_1 = Node::load(1);
$node_2 = Node::load(2);
$this->assertEquals('Node 1', $node_1->label());
$this->assertEquals('Node 2', $node_2->label());
}
}

View File

@@ -0,0 +1,94 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Functional\process;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\Tests\BrowserTestBase;
// cspell:ignore destid
/**
* Tests the 'download' process plugin.
*
* @group migrate
*/
class DownloadFunctionalTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'file'];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* Tests that an exception is thrown bu migration continues with the next row.
*/
public function testExceptionThrow(): void {
$invalid_url = "{$this->baseUrl}/not-existent-404";
$valid_url = "{$this->baseUrl}/core/misc/favicon.ico";
$definition = [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['url' => $invalid_url, 'uri' => 'public://first.txt'],
['url' => $valid_url, 'uri' => 'public://second.ico'],
],
'ids' => [
'url' => ['type' => 'string'],
],
],
'process' => [
'uri' => [
'plugin' => 'download',
'source' => ['url', 'uri'],
],
],
'destination' => [
'plugin' => 'entity:file',
],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$result = $executable->import();
// Check that the migration has completed.
$this->assertEquals(MigrationInterface::RESULT_COMPLETED, $result);
/** @var \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map_plugin */
$id_map_plugin = $migration->getIdMap();
// Check that the first row was marked as failed in the id map table.
$map_row = $id_map_plugin->getRowBySource(['url' => $invalid_url]);
$this->assertEquals(MigrateIdMapInterface::STATUS_FAILED, $map_row['source_row_status']);
$this->assertNull($map_row['destid1']);
// Check that a message with the thrown exception has been logged.
$messages = $id_map_plugin->getMessages(['url' => $invalid_url])->fetchAll();
$this->assertCount(1, $messages);
$message = reset($messages);
// Assert critical parts of the error message, but not the exact message,
// since it depends on Guzzle's internal implementation of PSR-7.
$id = $migration->getPluginId();
$this->assertStringContainsString("$id:uri:download:", $message->message);
$this->assertStringContainsString($invalid_url, $message->message);
$this->assertEquals(MigrationInterface::MESSAGE_ERROR, $message->level);
// Check that the second row was migrated successfully.
$map_row = $id_map_plugin->getRowBySource(['url' => $valid_url]);
$this->assertEquals(MigrateIdMapInterface::STATUS_IMPORTED, $map_row['source_row_status']);
$this->assertEquals(1, $map_row['destid1']);
}
}

View File

@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
/**
* Tests the high water handling.
*
* @covers \Drupal\migrate_high_water_test\Plugin\migrate\source\HighWaterTest
* @group migrate
*/
class HighWaterNotJoinableTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate',
'migrate_drupal',
'migrate_high_water_test',
];
/**
* {@inheritdoc}
*/
public static function providerSource() {
$tests = [];
// Test high water when the map is not joinable.
// The source data.
$tests[0]['source_data']['high_water_node'] = [
[
'id' => 1,
'title' => 'Item 1',
'changed' => 1,
],
[
'id' => 2,
'title' => 'Item 2',
'changed' => 2,
],
[
'id' => 3,
'title' => 'Item 3',
'changed' => 3,
],
];
// The expected results.
$tests[0]['expected_data'] = [
[
'id' => 2,
'title' => 'Item 2',
'changed' => 2,
],
[
'id' => 3,
'title' => 'Item 3',
'changed' => 3,
],
];
// The expected count is the count returned by the query before the query
// is modified by SqlBase::initializeIterator().
$tests[0]['expected_count'] = 3;
$tests[0]['configuration'] = [
'high_water_property' => [
'name' => 'changed',
],
];
$tests[0]['high_water'] = $tests[0]['source_data']['high_water_node'][0]['changed'];
// Test high water initialized to NULL.
$tests[1]['source_data'] = $tests[0]['source_data'];
$tests[1]['expected_data'] = [
[
'id' => 1,
'title' => 'Item 1',
'changed' => 1,
],
[
'id' => 2,
'title' => 'Item 2',
'changed' => 2,
],
[
'id' => 3,
'title' => 'Item 3',
'changed' => 3,
],
];
$tests[1]['expected_count'] = $tests[0]['expected_count'];
$tests[1]['configuration'] = $tests[0]['configuration'];
$tests[1]['high_water'] = NULL;
// Test high water initialized to an empty string.
$tests[2]['source_data'] = $tests[0]['source_data'];
$tests[2]['expected_data'] = [
[
'id' => 1,
'title' => 'Item 1',
'changed' => 1,
],
[
'id' => 2,
'title' => 'Item 2',
'changed' => 2,
],
[
'id' => 3,
'title' => 'Item 3',
'changed' => 3,
],
];
$tests[2]['expected_count'] = $tests[0]['expected_count'];
$tests[2]['configuration'] = $tests[0]['configuration'];
$tests[2]['high_water'] = '';
return $tests;
}
}

View File

@@ -0,0 +1,309 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
// cspell:ignore Highwater
/**
* Tests migration high water property.
*
* @group migrate
*/
class HighWaterTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'node',
'migrate',
'migrate_high_water_test',
'field',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create source test table.
$this->sourceDatabase->schema()->createTable('high_water_node', [
'fields' => [
'id' => [
'description' => 'Serial',
'type' => 'serial',
'unsigned' => TRUE,
'not null' => TRUE,
],
'changed' => [
'description' => 'Highwater',
'type' => 'int',
'unsigned' => TRUE,
],
'title' => [
'description' => 'Title',
'type' => 'varchar',
'length' => 128,
'not null' => TRUE,
'default' => '',
],
],
'primary key' => [
'id',
],
'description' => 'Contains nodes to import',
]);
// Add 3 items to source table.
$this->sourceDatabase->insert('high_water_node')
->fields([
'title',
'changed',
])
->values([
'title' => 'Item 1',
'changed' => 1,
])
->values([
'title' => 'Item 2',
'changed' => 2,
])
->values([
'title' => 'Item 3',
'changed' => 3,
])
->execute();
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installSchema('node', 'node_access');
$this->executeMigration('high_water_test');
}
/**
* Tests high water property of SqlBase.
*/
public function testHighWater(): void {
// Assert all of the nodes have been imported.
$this->assertNodeExists('Item 1');
$this->assertNodeExists('Item 2');
$this->assertNodeExists('Item 3');
// Update Item 1 setting its high_water_property to value that is below
// current high water mark.
$this->sourceDatabase->update('high_water_node')
->fields([
'title' => 'Item 1 updated',
'changed' => 2,
])
->condition('title', 'Item 1')
->execute();
// Update Item 2 setting its high_water_property to value equal to
// current high water mark.
$this->sourceDatabase->update('high_water_node')
->fields([
'title' => 'Item 2 updated',
'changed' => 3,
])
->condition('title', 'Item 2')
->execute();
// Update Item 3 setting its high_water_property to value that is above
// current high water mark.
$this->sourceDatabase->update('high_water_node')
->fields([
'title' => 'Item 3 updated',
'changed' => 4,
])
->condition('title', 'Item 3')
->execute();
// Execute migration again.
$this->executeMigration('high_water_test');
// Item with lower high water should not be updated.
$this->assertNodeExists('Item 1');
$this->assertNodeDoesNotExist('Item 1 updated');
// Item with equal high water should not be updated.
$this->assertNodeExists('Item 2');
$this->assertNodeDoesNotExist('Item 2 updated');
// Item with greater high water should be updated.
$this->assertNodeExists('Item 3 updated');
$this->assertNodeDoesNotExist('Item 3');
}
/**
* Tests that the high water value can be 0.
*/
public function testZeroHighwater(): void {
// Assert all of the nodes have been imported.
$this->assertNodeExists('Item 1');
$this->assertNodeExists('Item 2');
$this->assertNodeExists('Item 3');
$migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
$source = $migration->getSourcePlugin();
$source->rewind();
$count = 0;
while ($source->valid()) {
$count++;
$source->next();
}
// Expect no rows as everything is below the high water mark.
$this->assertSame(0, $count);
// Test resetting the high water mark to 0.
$this->container->get('keyvalue')->get('migrate:high_water')->set('high_water_test', 0);
$migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
$source = $migration->getSourcePlugin();
$source->rewind();
$count = 0;
while ($source->valid()) {
$count++;
$source->next();
}
$this->assertSame(3, $count);
}
/**
* Tests that deleting the high water value causes all rows to be reimported.
*/
public function testNullHighwater(): void {
// Assert all of the nodes have been imported.
$this->assertNodeExists('Item 1');
$this->assertNodeExists('Item 2');
$this->assertNodeExists('Item 3');
$migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
$source = $migration->getSourcePlugin();
$source->rewind();
$count = 0;
while ($source->valid()) {
$count++;
$source->next();
}
// Expect no rows as everything is below the high water mark.
$this->assertSame(0, $count);
// Test resetting the high water mark.
$this->container->get('keyvalue')->get('migrate:high_water')->delete('high_water_test');
$migration = $this->container->get('plugin.manager.migration')->CreateInstance('high_water_test', []);
$source = $migration->getSourcePlugin();
$source->rewind();
$count = 0;
while ($source->valid()) {
$count++;
$source->next();
}
$this->assertSame(3, $count);
}
/**
* Tests high water property of SqlBase when rows marked for update.
*/
public function testHighWaterUpdate(): void {
// Assert all of the nodes have been imported.
$this->assertNodeExists('Item 1');
$this->assertNodeExists('Item 2');
$this->assertNodeExists('Item 3');
// Update Item 1 setting its high_water_property to value that is below
// current high water mark.
$this->sourceDatabase->update('high_water_node')
->fields([
'title' => 'Item 1 updated',
'changed' => 2,
])
->condition('title', 'Item 1')
->execute();
// Update Item 2 setting its high_water_property to value equal to
// current high water mark.
$this->sourceDatabase->update('high_water_node')
->fields([
'title' => 'Item 2 updated',
'changed' => 3,
])
->condition('title', 'Item 2')
->execute();
// Update Item 3 setting its high_water_property to value that is above
// current high water mark.
$this->sourceDatabase->update('high_water_node')
->fields([
'title' => 'Item 3 updated',
'changed' => 4,
])
->condition('title', 'Item 3')
->execute();
// Set all rows as needing an update.
$id_map = $this->getMigration('high_water_test')->getIdMap();
$id_map->prepareUpdate();
$this->executeMigration('high_water_test');
// Item with lower high water should be updated.
$this->assertNodeExists('Item 1 updated');
$this->assertNodeDoesNotExist('Item 1');
// Item with equal high water should be updated.
$this->assertNodeExists('Item 2 updated');
$this->assertNodeDoesNotExist('Item 2');
// Item with greater high water should be updated.
$this->assertNodeExists('Item 3 updated');
$this->assertNodeDoesNotExist('Item 3');
}
/**
* Assert that node with given title exists.
*
* @param string $title
* Title of the node.
*
* @internal
*/
protected function assertNodeExists(string $title): void {
self::assertTrue($this->nodeExists($title));
}
/**
* Assert that node with given title does not exist.
*
* @param string $title
* Title of the node.
*
* @internal
*/
protected function assertNodeDoesNotExist(string $title): void {
self::assertFalse($this->nodeExists($title));
}
/**
* Checks if node with given title exists.
*
* @param string $title
* Title of the node.
*
* @return bool
*/
protected function nodeExists($title) {
$query = \Drupal::entityQuery('node')->accessCheck(FALSE);
$result = $query
->condition('title', $title)
->range(0, 1)
->execute();
return !empty($result);
}
}

View File

@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateExecutable;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Tests setting of bundles on content entity migrations.
*
* @group migrate
*/
class MigrateBundleTest extends MigrateTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['taxonomy', 'text', 'user', 'system'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('taxonomy_term');
$this->installConfig(['taxonomy']);
// Set up two vocabularies (taxonomy bundles).
Vocabulary::create(['vid' => 'tags', 'name' => 'Tags']);
Vocabulary::create(['vid' => 'categories', 'name' => 'Categories']);
}
/**
* Tests setting the bundle in the destination.
*/
public function testDestinationBundle(): void {
$term_data_rows = [
['id' => 1, 'name' => 'Category 1'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'terms',
'migration_tags' => ['Bundle test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $term_data_rows,
'ids' => $ids,
],
'process' => [
'tid' => 'id',
'name' => 'name',
],
'destination' => [
'plugin' => 'entity:taxonomy_term',
'default_bundle' => 'categories',
],
'migration_dependencies' => [],
];
$term_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
// Import and validate the term entity was created with the correct bundle.
$term_executable = new MigrateExecutable($term_migration, $this);
$term_executable->import();
/** @var \Drupal\taxonomy\Entity\Term $term */
$term = Term::load(1);
$this->assertEquals('categories', $term->bundle());
}
/**
* Tests setting the bundle in the process pipeline.
*/
public function testProcessBundle(): void {
$term_data_rows = [
['id' => 1, 'vocab' => 'categories', 'name' => 'Category 1'],
['id' => 2, 'vocab' => 'tags', 'name' => 'Tag 1'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'terms',
'migration_tags' => ['Bundle test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $term_data_rows,
'ids' => $ids,
],
'process' => [
'tid' => 'id',
'vid' => 'vocab',
'name' => 'name',
],
'destination' => [
'plugin' => 'entity:taxonomy_term',
],
'migration_dependencies' => [],
];
$term_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
// Import and validate the term entities were created with the correct bundle.
$term_executable = new MigrateExecutable($term_migration, $this);
$term_executable->import();
/** @var \Drupal\taxonomy\Entity\Term $term */
$term = Term::load(1);
$this->assertEquals('categories', $term->bundle());
$term = Term::load(2);
$this->assertEquals('tags', $term->bundle());
}
/**
* Tests setting bundles both in process and destination.
*/
public function testMixedBundles(): void {
$term_data_rows = [
['id' => 1, 'vocab' => 'categories', 'name' => 'Category 1'],
['id' => 2, 'name' => 'Tag 1'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'terms',
'migration_tags' => ['Bundle test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $term_data_rows,
'ids' => $ids,
],
'process' => [
'tid' => 'id',
'vid' => 'vocab',
'name' => 'name',
],
'destination' => [
'plugin' => 'entity:taxonomy_term',
// When no vocab is provided, the destination bundle is applied.
'default_bundle' => 'tags',
],
'migration_dependencies' => [],
];
$term_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
// Import and validate the term entities were created with the correct bundle.
$term_executable = new MigrateExecutable($term_migration, $this);
$term_executable->import();
/** @var \Drupal\taxonomy\Entity\Term $term */
$term = Term::load(1);
$this->assertEquals('categories', $term->bundle());
$term = Term::load(2);
$this->assertEquals('tags', $term->bundle());
}
}

View File

@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateExecutable;
/**
* Tests rolling back of configuration objects.
*
* @group migrate
*/
class MigrateConfigRollbackTest extends MigrateTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['system', 'language', 'config_translation'];
/**
* Tests rolling back configuration.
*/
public function testConfigRollback(): void {
// Use system.site configuration to demonstrate importing and rolling back
// configuration.
$variable = [
[
'id' => 'site_name',
'site_name' => 'Some site',
'site_slogan' => 'Awesome slogan',
],
];
$ids = [
'id' =>
[
'type' => 'string',
],
];
$definition = [
'id' => 'config',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $variable,
'ids' => $ids,
],
'process' => [
'name' => 'site_name',
'slogan' => 'site_slogan',
],
'destination' => [
'plugin' => 'config',
'config_name' => 'system.site',
],
];
/** @var \Drupal\migrate\Plugin\Migration $config_migration */
$config_migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$config_id_map = $config_migration->getIdMap();
// Rollback is not enabled for configuration translations.
$this->assertFalse($config_migration->getDestinationPlugin()->supportsRollback());
// Import and validate config entities were created.
$config_executable = new MigrateExecutable($config_migration, $this);
$config_executable->import();
$config = $this->config('system.site');
$this->assertSame('Some site', $config->get('name'));
$this->assertSame('Awesome slogan', $config->get('slogan'));
$map_row = $config_id_map->getRowBySource(['id' => $variable[0]['id']]);
$this->assertNotNull($map_row['destid1']);
// Rollback and verify the configuration changes are still there.
$config_executable->rollback();
$config = $this->config('system.site');
$this->assertSame('Some site', $config->get('name'));
$this->assertSame('Awesome slogan', $config->get('slogan'));
// Confirm the map row is deleted.
$this->assertFalse($config_id_map->getRowBySource(['id' => $variable[0]['id']]));
// We use system configuration to demonstrate importing and rolling back
// configuration translations.
$i18n_variable = [
[
'id' => 'site_name',
'language' => 'fr',
'site_name' => 'fr - Some site',
'site_slogan' => 'fr - Awesome slogan',
],
[
'id' => 'site_name',
'language' => 'is',
'site_name' => 'is - Some site',
'site_slogan' => 'is - Awesome slogan',
],
];
$ids = [
'id' =>
[
'type' => 'string',
],
'language' =>
[
'type' => 'string',
],
];
$definition = [
'id' => 'i18n_config',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $i18n_variable,
'ids' => $ids,
],
'process' => [
'langcode' => 'language',
'name' => 'site_name',
'slogan' => 'site_slogan',
],
'destination' => [
'plugin' => 'config',
'config_name' => 'system.site',
'translations' => 'true',
],
];
$config_migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$config_id_map = $config_migration->getIdMap();
// Rollback is enabled for configuration translations.
$this->assertTrue($config_migration->getDestinationPlugin()->supportsRollback());
// Import and validate config entities were created.
$config_executable = new MigrateExecutable($config_migration, $this);
$config_executable->import();
$language_manager = \Drupal::service('language_manager');
foreach ($i18n_variable as $row) {
$langcode = $row['language'];
/** @var \Drupal\language\Config\LanguageConfigOverride $config_translation */
$config_translation = $language_manager->getLanguageConfigOverride($langcode, 'system.site');
$this->assertSame($row['site_name'], $config_translation->get('name'));
$this->assertSame($row['site_slogan'], $config_translation->get('slogan'));
$map_row = $config_id_map->getRowBySource(['id' => $row['id'], 'language' => $row['language']]);
$this->assertNotNull($map_row['destid1']);
}
// Rollback and verify the translation have been removed.
$config_executable->rollback();
foreach ($i18n_variable as $row) {
$langcode = $row['language'];
$config_translation = $language_manager->getLanguageConfigOverride($langcode, 'system.site');
$this->assertNull($config_translation->get('name'));
$this->assertNull($config_translation->get('slogan'));
// Confirm the map row is deleted.
$map_row = $config_id_map->getRowBySource(['id' => $row['id'], 'language' => $langcode]);
$this->assertFalse($map_row);
}
// Test that the configuration is still present.
$config = $this->config('system.site');
$this->assertSame('Some site', $config->get('name'));
$this->assertSame('Awesome slogan', $config->get('slogan'));
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Allows tests to alter dumps after they have loaded.
*
* @see \Drupal\migrate_drupal\Tests\d6\MigrateFileTest
*/
interface MigrateDumpAlterInterface {
/**
* Allows tests to alter dumps after they have loaded.
*
* @param \Drupal\KernelTests\KernelTestBase $test
* The test that is being run.
*/
public static function migrateDumpAlter(KernelTestBase $test);
}

View File

@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the EmbeddedDataSource plugin.
*
* @group migrate
*/
class MigrateEmbeddedDataTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['migrate'];
/**
* Tests the embedded_data source plugin.
*/
public function testEmbeddedData(): void {
$data_rows = [
['key' => '1', 'field1' => 'f1value1', 'field2' => 'f2value1'],
['key' => '2', 'field1' => 'f1value2', 'field2' => 'f2value2'],
];
$ids = ['key' => ['type' => 'integer']];
$definition = [
'migration_tags' => ['Embedded data test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $data_rows,
'ids' => $ids,
],
'process' => [],
'destination' => ['plugin' => 'null'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$source = $migration->getSourcePlugin();
// Validate the plugin returns the source data that was provided.
$results = [];
/** @var \Drupal\migrate\Row $row */
foreach ($source as $row) {
$this->assertFalse($row->isStub());
$data_row = $row->getSource();
// The "data" row returned by getSource() also includes all source
// configuration - we remove it so we see only the data itself.
unset($data_row['plugin']);
unset($data_row['data_rows']);
unset($data_row['ids']);
$results[] = $data_row;
}
$this->assertSame($data_rows, $results);
// Validate the public APIs.
$this->assertSameSize($data_rows, $source);
$this->assertSame($ids, $source->getIds());
$expected_fields = [
'key' => 'key',
'field1' => 'field1',
'field2' => 'field2',
];
$this->assertSame($expected_fields, $source->fields());
}
}

View File

@@ -0,0 +1,357 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\Core\Entity\EntityFieldManager;
use Drupal\entity_test\Entity\EntityTestMul;
use Drupal\KernelTests\KernelTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\migrate\destination\EntityContentBase;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Tests\StubTestTrait;
use Drupal\migrate_entity_test\Entity\StringIdEntityTest;
/**
* Tests the EntityContentBase destination.
*
* @group migrate
*/
class MigrateEntityContentBaseTest extends KernelTestBase {
use StubTestTrait;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['migrate', 'user', 'language', 'entity_test'];
/**
* The storage for entity_test_mul.
*
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
*/
protected $storage;
/**
* A content migrate destination.
*
* @var \Drupal\migrate\Plugin\MigrateDestinationInterface
*/
protected $destination;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Enable two required fields with default values: a single-value field and
// a multi-value field.
\Drupal::state()->set('entity_test.required_default_field', TRUE);
\Drupal::state()->set('entity_test.required_multi_default_field', TRUE);
$this->installEntitySchema('entity_test_mul');
ConfigurableLanguage::createFromLangcode('en')->save();
ConfigurableLanguage::createFromLangcode('fr')->save();
$this->storage = $this->container->get('entity_type.manager')->getStorage('entity_test_mul');
}
/**
* Check the existing translations of an entity.
*
* @param int $id
* The entity ID.
* @param string $default
* The expected default translation language code.
* @param string[] $others
* The expected other translation language codes.
*
* @internal
*/
protected function assertTranslations(int $id, string $default, array $others = []): void {
$entity = $this->storage->load($id);
$this->assertNotEmpty($entity, "Entity exists");
$this->assertEquals($default, $entity->language()->getId(), "Entity default translation");
$translations = array_keys($entity->getTranslationLanguages(FALSE));
sort($others);
sort($translations);
$this->assertEquals($others, $translations, "Entity translations");
}
/**
* Create the destination plugin to test.
*
* @param array $configuration
* The plugin configuration.
*/
protected function createDestination(array $configuration) {
$this->destination = new EntityContentBase(
$configuration,
'fake_plugin_id',
[],
$this->createMock(MigrationInterface::class),
$this->storage,
[],
$this->container->get('entity_field.manager'),
$this->container->get('plugin.manager.field.field_type'),
$this->container->get('account_switcher')
);
}
/**
* Tests importing and rolling back translated entities.
*/
public function testTranslated(): void {
// Create a destination.
$this->createDestination(['translations' => TRUE]);
// Create some pre-existing entities.
$this->storage->create(['id' => 1, 'langcode' => 'en'])->save();
$this->storage->create(['id' => 2, 'langcode' => 'fr'])->save();
$translated = $this->storage->create(['id' => 3, 'langcode' => 'en']);
$translated->save();
$translated->addTranslation('fr')->save();
// Pre-assert that things are as expected.
$this->assertTranslations(1, 'en');
$this->assertTranslations(2, 'fr');
$this->assertTranslations(3, 'en', ['fr']);
$this->assertNull($this->storage->load(4));
$destination_rows = [
// Existing default translation.
['id' => 1, 'langcode' => 'en', 'action' => MigrateIdMapInterface::ROLLBACK_PRESERVE],
// New translation.
['id' => 2, 'langcode' => 'en', 'action' => MigrateIdMapInterface::ROLLBACK_DELETE],
// Existing non-default translation.
['id' => 3, 'langcode' => 'fr', 'action' => MigrateIdMapInterface::ROLLBACK_PRESERVE],
// Brand new row.
['id' => 4, 'langcode' => 'fr', 'action' => MigrateIdMapInterface::ROLLBACK_DELETE],
];
$rollback_actions = [];
// Import some rows.
foreach ($destination_rows as $idx => $destination_row) {
$row = new Row();
foreach ($destination_row as $key => $value) {
$row->setDestinationProperty($key, $value);
}
$this->destination->import($row);
// Check that the rollback action is correct, and save it.
$this->assertEquals($destination_row['action'], $this->destination->rollbackAction());
$rollback_actions[$idx] = $this->destination->rollbackAction();
}
$this->assertTranslations(1, 'en');
$this->assertTranslations(2, 'fr', ['en']);
$this->assertTranslations(3, 'en', ['fr']);
$this->assertTranslations(4, 'fr');
// Rollback the rows.
foreach ($destination_rows as $idx => $destination_row) {
if ($rollback_actions[$idx] == MigrateIdMapInterface::ROLLBACK_DELETE) {
$this->destination->rollback($destination_row);
}
}
// No change, update of existing translation.
$this->assertTranslations(1, 'en');
// Remove added translation.
$this->assertTranslations(2, 'fr');
// No change, update of existing translation.
$this->assertTranslations(3, 'en', ['fr']);
// No change, can't remove default translation.
$this->assertTranslations(4, 'fr');
}
/**
* Tests creation of ID columns table with definitions taken from entity type.
*/
public function testEntityWithStringId(): void {
$this->enableModules(['migrate_entity_test']);
$this->installEntitySchema('migrate_string_id_entity_test');
$definition = [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['id' => 123, 'version' => 'foo'],
// This integer needs an 'int' schema with 'big' size. If 'destid1'
// is not correctly taking the definition from the destination entity
// type, the import will fail with a SQL exception.
['id' => 123456789012, 'version' => 'bar'],
],
'ids' => [
'id' => ['type' => 'integer', 'size' => 'big'],
'version' => ['type' => 'string'],
],
],
'process' => [
'id' => 'id',
'version' => 'version',
],
'destination' => [
'plugin' => 'entity:migrate_string_id_entity_test',
],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$result = $executable->import();
$this->assertEquals(MigrationInterface::RESULT_COMPLETED, $result);
/** @var \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map_plugin */
$id_map_plugin = $migration->getIdMap();
// Check that the destination has been stored.
$map_row = $id_map_plugin->getRowBySource(['id' => 123, 'version' => 'foo']);
$this->assertEquals(123, $map_row['destid1']);
$map_row = $id_map_plugin->getRowBySource(['id' => 123456789012, 'version' => 'bar']);
$this->assertEquals(123456789012, $map_row['destid1']);
}
/**
* Tests empty destinations.
*/
public function testEmptyDestinations(): void {
$this->enableModules(['migrate_entity_test']);
$this->installEntitySchema('migrate_string_id_entity_test');
$definition = [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['id' => 123, 'version' => 'foo'],
// This integer needs an 'int' schema with 'big' size. If 'destid1'
// is not correctly taking the definition from the destination entity
// type, the import will fail with an SQL exception.
['id' => 123456789012, 'version' => 'bar'],
],
'ids' => [
'id' => ['type' => 'integer', 'size' => 'big'],
'version' => ['type' => 'string'],
],
'constants' => ['null' => NULL],
],
'process' => [
'id' => 'id',
'version' => 'version',
],
'destination' => [
'plugin' => 'entity:migrate_string_id_entity_test',
],
];
$migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$executable->import();
/** @var \Drupal\migrate_entity_test\Entity\StringIdEntityTest $entity */
$entity = StringIdEntityTest::load('123');
$this->assertSame('foo', $entity->version->value);
$entity = StringIdEntityTest::load('123456789012');
$this->assertSame('bar', $entity->version->value);
// Rerun the migration forcing the version to NULL.
$definition['process'] = [
'id' => 'id',
'version' => 'constants/null',
];
$migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$executable->import();
/** @var \Drupal\migrate_entity_test\Entity\StringIdEntityTest $entity */
$entity = StringIdEntityTest::load('123');
$this->assertNull($entity->version->value);
$entity = StringIdEntityTest::load('123456789012');
$this->assertNull($entity->version->value);
}
/**
* Tests stub rows.
*/
public function testStubRows(): void {
// Create a destination.
$this->createDestination([]);
// Import a stub row.
$row = new Row([], [], TRUE);
$row->setDestinationProperty('type', 'test');
$ids = $this->destination->import($row);
$this->assertCount(1, $ids);
// Make sure the entity was saved.
$entity = EntityTestMul::load(reset($ids));
$this->assertInstanceOf(EntityTestMul::class, $entity);
// Make sure the default value was applied to the required fields.
$single_field_name = 'required_default_field';
$single_default_value = $entity->getFieldDefinition($single_field_name)->getDefaultValueLiteral();
$this->assertSame($single_default_value, $entity->get($single_field_name)->getValue());
$multi_field_name = 'required_multi_default_field';
$multi_default_value = $entity->getFieldDefinition($multi_field_name)->getDefaultValueLiteral();
$count = 3;
$this->assertCount($count, $multi_default_value);
for ($i = 0; $i < $count; ++$i) {
$this->assertSame($multi_default_value[$i], $entity->get($multi_field_name)->get($i)->getValue());
}
}
/**
* Tests bundle is properly provided for stubs without bundle support.
*
* @todo Remove this test in when native PHP type-hints will be added for
* EntityFieldManagerInterface::getFieldDefinitions(). See
* https://www.drupal.org/project/drupal/issues/3050720.
*/
public function testBundleFallbackForStub(): void {
$this->enableModules(['migrate_entity_test']);
$this->installEntitySchema('migrate_string_id_entity_test');
$entity_type_manager = $this->container->get('entity_type.manager');
$entity_type_bundle_info = $this->container->get('entity_type.bundle.info');
$entity_display_repository = $this
->container
->get('entity_display.repository');
$typed_data_manager = $this->container->get('typed_data_manager');
$language_manager = $this->container->get('language_manager');
$keyvalue = $this->container->get('keyvalue');
$module_handler = $this->container->get('module_handler');
$cache_discovery = $this->container->get('cache.discovery');
$entity_last_installed_schema_repository = $this
->container
->get('entity.last_installed_schema.repository');
$decorated_entity_field_manager = new class ($entity_type_manager, $entity_type_bundle_info, $entity_display_repository, $typed_data_manager, $language_manager, $keyvalue, $module_handler, $cache_discovery, $entity_last_installed_schema_repository) extends EntityFieldManager {
/**
* {@inheritdoc}
*/
public function getFieldDefinitions($entity_type_id, $bundle) {
if (\is_null($bundle)) {
throw new \Exception("Bundle value shouldn't be NULL.");
}
return parent::getFieldDefinitions($entity_type_id, $bundle);
}
};
$this->container->set('entity_field.manager', $decorated_entity_field_manager);
$this->createEntityStub('migrate_string_id_entity_test');
}
}

View File

@@ -0,0 +1,276 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\filter\Entity\FilterFormat;
use Drupal\filter\FilterFormatInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateIdMapMessageEvent;
use Drupal\migrate\MigrateExecutable;
use Drupal\user\Entity\Role;
use Drupal\user\Entity\User;
use Drupal\user\Plugin\Validation\Constraint\UserNameConstraint;
use Drupal\user\RoleInterface;
/**
* Tests validation of an entity during migration.
*
* @group migrate
*/
class MigrateEntityContentValidationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_test',
'field',
'filter',
'filter_test',
'migrate',
'system',
'text',
'user',
];
/**
* Messages accumulated during the migration run.
*
* @var string[]
*/
protected $messages = [];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('user_role');
$this->installEntitySchema('entity_test');
$this->installConfig(['field', 'filter_test', 'system', 'user']);
$this->container
->get('event_dispatcher')
->addListener(MigrateEvents::IDMAP_MESSAGE, [$this, 'mapMessageRecorder']);
}
/**
* Tests an import with invalid data and checks error messages.
*/
public function test1(): void {
// Make sure that a user with uid 2 exists.
$this->container
->get('entity_type.manager')
->getStorage('user')
->create([
'uid' => 2,
'name' => $this->randomMachineName(),
'status' => 1,
])
->save();
$this->runImport([
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
[
'id' => '1',
'name' => $this->randomString(256),
'user_id' => '1',
],
[
'id' => '2',
'name' => $this->randomString(64),
'user_id' => '1',
],
[
'id' => '3',
'name' => $this->randomString(64),
'user_id' => '2',
],
],
'ids' => [
'id' => ['type' => 'integer'],
],
],
'process' => [
'id' => 'id',
'name' => 'name',
'user_id' => 'user_id',
],
'destination' => [
'plugin' => 'entity:entity_test',
'validate' => TRUE,
],
]);
$this->assertSame('1: [entity_test: 1]: name.0.value=<em class="placeholder">Name</em>: may not be longer than 64 characters.||user_id.0.target_id=The referenced entity (<em class="placeholder">user</em>: <em class="placeholder">1</em>) does not exist.', $this->messages[0], 'First message should have 2 validation errors.');
$this->assertSame('2: [entity_test: 2]: user_id.0.target_id=The referenced entity (<em class="placeholder">user</em>: <em class="placeholder">1</em>) does not exist.', $this->messages[1], 'Second message should have 1 validation error.');
$this->assertArrayNotHasKey(2, $this->messages, 'Third message should not exist.');
}
/**
* Tests an import with invalid data and checks error messages.
*/
public function test2(): void {
$long_username = $this->randomString(61);
$username_constraint = new UserNameConstraint();
$this->runImport([
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
[
'id' => 1,
'name' => $long_username,
],
[
'id' => 2,
'name' => $this->randomString(32),
],
[
'id' => 3,
'name' => $this->randomString(32),
],
],
'ids' => [
'id' => ['type' => 'integer'],
],
],
'process' => [
'name' => 'name',
],
'destination' => [
'plugin' => 'entity:user',
'validate' => TRUE,
],
]);
$this->assertSame(sprintf('1: [user]: name=%s||name=%s||mail=Email field is required.', $username_constraint->illegalMessage, t($username_constraint->tooLongMessage, ['%name' => $long_username, '%max' => 60])), $this->messages[0], 'First message should have 3 validation errors.');
$this->assertSame(sprintf('2: [user]: name=%s||mail=Email field is required.', $username_constraint->illegalMessage), $this->messages[1], 'Second message should have 2 validation errors.');
$this->assertSame(sprintf('3: [user]: name=%s||mail=Email field is required.', $username_constraint->illegalMessage), $this->messages[2], 'Third message should have 2 validation errors.');
$this->assertArrayNotHasKey(3, $this->messages, 'Fourth message should not exist.');
}
/**
* Tests validation for entities that are instances of EntityOwnerInterface.
*/
public function testEntityOwnerValidation(): void {
// Text format access is impacted by user permissions.
$filter_test_format = FilterFormat::load('filter_test');
assert($filter_test_format instanceof FilterFormatInterface);
// Create 2 users, an admin user who has permission to use this text format
// and another who does not have said access.
$role = Role::create([
'id' => 'admin',
'label' => 'admin',
'is_admin' => TRUE,
]);
assert($role instanceof RoleInterface);
$role->grantPermission($filter_test_format->getPermissionName());
$role->save();
$admin_user = User::create([
'name' => 'foobar',
'mail' => 'foobar@example.com',
]);
$admin_user->addRole($role->id())->save();
$normal_user = User::create([
'name' => 'normal user',
'mail' => 'normal@example.com',
]);
$normal_user->save();
// Add a "body" field with the text format.
$field_name = $this->randomMachineName();
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'entity_test',
'type' => 'text',
]);
$field_storage->save();
FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'entity_test',
])->save();
// Attempt to migrate entities. The first record is owned by an admin user.
$definition = [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
[
'id' => 1,
'uid' => $admin_user->id(),
'body' => [
'value' => 'foo',
'format' => 'filter_test',
],
],
[
'id' => 2,
'uid' => $normal_user->id(),
'body' => [
'value' => 'bar',
'format' => 'filter_test',
],
],
],
'ids' => [
'id' => ['type' => 'integer'],
],
],
'process' => [
'id' => 'id',
'user_id' => 'uid',
"$field_name/value" => 'body/value',
"$field_name/format" => 'body/format',
],
'destination' => [
'plugin' => 'entity:entity_test',
'validate' => TRUE,
],
];
$this->container->get('current_user')->setAccount($normal_user);
$this->runImport($definition);
// The second user import should fail validation because they do not have
// access to use "filter_test" filter.
$this->assertSame(sprintf('2: [entity_test: 2]: user_id.0.target_id=This entity (<em class="placeholder">user</em>: <em class="placeholder">%s</em>) cannot be referenced.||%s.0.format=The value you selected is not a valid choice.', $normal_user->id(), $field_name), $this->messages[0]);
$this->assertArrayNotHasKey(1, $this->messages);
}
/**
* Reacts to map message event.
*
* @param \Drupal\migrate\Event\MigrateIdMapMessageEvent $event
* The migration event.
*/
public function mapMessageRecorder(MigrateIdMapMessageEvent $event) {
$this->messages[] = implode(',', $event->getSourceIdValues()) . ': ' . $event->getMessage();
}
/**
* Runs an import of a migration.
*
* @param array $definition
* The migration definition.
*
* @throws \Exception
* @throws \Drupal\migrate\MigrateException
*/
protected function runImport(array $definition) {
// Reset the list of messages from a previous migration.
$this->messages = [];
(new MigrateExecutable($this->container->get('plugin.manager.migration')->createStubMigration($definition)))->import();
}
}

View File

@@ -0,0 +1,219 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\Event\MigrateImportEvent;
use Drupal\migrate\Event\MigrateMapDeleteEvent;
use Drupal\migrate\Event\MigrateMapSaveEvent;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Drupal\migrate\Event\MigratePreRowSaveEvent;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\MigrateExecutable;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests events fired on migrations.
*
* @group migrate
*/
class MigrateEventsTest extends KernelTestBase {
/**
* State service for recording information received by event listeners.
*
* @var \Drupal\Core\State\State
*/
protected $state;
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['migrate', 'migrate_events_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->state = \Drupal::state();
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::MAP_SAVE,
[$this, 'mapSaveEventRecorder']);
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::MAP_DELETE,
[$this, 'mapDeleteEventRecorder']);
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::PRE_IMPORT,
[$this, 'preImportEventRecorder']);
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::POST_IMPORT,
[$this, 'postImportEventRecorder']);
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::PRE_ROW_SAVE,
[$this, 'preRowSaveEventRecorder']);
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::POST_ROW_SAVE,
[$this, 'postRowSaveEventRecorder']);
}
/**
* Tests migration events.
*/
public function testMigrateEvents(): void {
// Run a simple little migration, which should trigger one of each event
// other than map_delete.
$definition = [
'migration_tags' => ['Event test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['data' => 'dummy value'],
],
'ids' => [
'data' => ['type' => 'string'],
],
],
'process' => ['value' => 'data'],
'destination' => ['plugin' => 'dummy'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
// As the import runs, events will be dispatched, recording the received
// information in state.
$executable->import();
// Validate from the recorded state that the events were received.
$event = $this->state->get('migrate_events_test.pre_import_event', []);
$this->assertSame(MigrateEvents::PRE_IMPORT, $event['event_name']);
$this->assertSame($migration->id(), $event['migration']->id());
$event = $this->state->get('migrate_events_test.post_import_event', []);
$this->assertSame(MigrateEvents::POST_IMPORT, $event['event_name']);
$this->assertSame($migration->id(), $event['migration']->id());
$event = $this->state->get('migrate_events_test.map_save_event', []);
$this->assertSame(MigrateEvents::MAP_SAVE, $event['event_name']);
// Validating the last row processed.
$this->assertSame('dummy value', $event['fields']['sourceid1']);
$this->assertSame('dummy value', $event['fields']['destid1']);
$this->assertSame(0, $event['fields']['source_row_status']);
$event = $this->state->get('migrate_events_test.map_delete_event', []);
$this->assertSame([], $event);
$event = $this->state->get('migrate_events_test.pre_row_save_event', []);
$this->assertSame(MigrateEvents::PRE_ROW_SAVE, $event['event_name']);
$this->assertSame($migration->id(), $event['migration']->id());
// Validating the last row processed.
$this->assertSame('dummy value', $event['row']->getSourceProperty('data'));
$event = $this->state->get('migrate_events_test.post_row_save_event', []);
$this->assertSame(MigrateEvents::POST_ROW_SAVE, $event['event_name']);
$this->assertSame($migration->id(), $event['migration']->id());
// Validating the last row processed.
$this->assertSame('dummy value', $event['row']->getSourceProperty('data'));
$this->assertSame('dummy value', $event['destination_id_values']['value']);
// Generate a map delete event.
$migration->getIdMap()->delete(['data' => 'dummy value']);
$event = $this->state->get('migrate_events_test.map_delete_event', []);
$this->assertSame(MigrateEvents::MAP_DELETE, $event['event_name']);
$this->assertSame(['data' => 'dummy value'], $event['source_id']);
}
/**
* Reacts to map save event.
*
* @param \Drupal\migrate\Event\MigrateMapSaveEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function mapSaveEventRecorder(MigrateMapSaveEvent $event, $name) {
$this->state->set('migrate_events_test.map_save_event', [
'event_name' => $name,
'map' => $event->getMap(),
'fields' => $event->getFields(),
]);
}
/**
* Reacts to map delete event.
*
* @param \Drupal\migrate\Event\MigrateMapDeleteEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function mapDeleteEventRecorder(MigrateMapDeleteEvent $event, $name) {
$this->state->set('migrate_events_test.map_delete_event', [
'event_name' => $name,
'map' => $event->getMap(),
'source_id' => $event->getSourceId(),
]);
}
/**
* Reacts to pre-import event.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function preImportEventRecorder(MigrateImportEvent $event, $name) {
$this->state->set('migrate_events_test.pre_import_event', [
'event_name' => $name,
'migration' => $event->getMigration(),
]);
}
/**
* Reacts to post-import event.
*
* @param \Drupal\migrate\Event\MigrateImportEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function postImportEventRecorder(MigrateImportEvent $event, $name) {
$this->state->set('migrate_events_test.post_import_event', [
'event_name' => $name,
'migration' => $event->getMigration(),
]);
}
/**
* Reacts to pre-row-save event.
*
* @param \Drupal\migrate\Event\MigratePreRowSaveEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function preRowSaveEventRecorder(MigratePreRowSaveEvent $event, $name) {
$this->state->set('migrate_events_test.pre_row_save_event', [
'event_name' => $name,
'migration' => $event->getMigration(),
'row' => $event->getRow(),
]);
}
/**
* Reacts to post-row-save event.
*
* @param \Drupal\migrate\Event\MigratePostRowSaveEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function postRowSaveEventRecorder(MigratePostRowSaveEvent $event, $name) {
$this->state->set('migrate_events_test.post_row_save_event', [
'event_name' => $name,
'migration' => $event->getMigration(),
'row' => $event->getRow(),
'destination_id_values' => $event->getDestinationIdValues(),
]);
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests the MigrateExecutable class.
*
* @group migrate
*/
class MigrateExecutableTest extends MigrateTestBase {
protected static $modules = [
'entity_test',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('entity_test');
}
/**
* Tests the MigrateExecutable class.
*/
public function testMigrateExecutable(): void {
$data_rows = [
['key' => '1', 'field1' => 'f1value1', 'field2' => 'f2value1'],
['key' => '2', 'field1' => 'f1value2', 'field2' => 'f2value2'],
];
$ids = ['key' => ['type' => 'integer']];
$definition = [
'migration_tags' => ['Embedded data test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $data_rows,
'ids' => $ids,
],
'process' => [],
'destination' => ['plugin' => 'entity:entity_test'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new TestMigrateExecutable($migration);
$this->assertEquals(MigrationInterface::RESULT_COMPLETED, $executable->import());
// Test the exception message when a process plugin throws a
// MigrateSkipRowException. Change the definition to have one data row and a
// process that will throw a MigrateSkipRowException on every row.
$definition['source']['data_rows'] = [
[
'key' => '1',
'field1' => 'f1value1',
],
];
$definition['process'] = [
'foo' => [
'plugin' => 'skip_row_if_not_set',
'index' => 'foo',
'source' => 'field1',
'message' => 'test message',
],
];
$migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$executable = new TestMigrateExecutable($migration);
$executable->import();
$messages = iterator_to_array($migration->getIdMap()->getMessages());
$this->assertCount(1, $messages);
$expected = $migration->getPluginId() . ':foo: test message';
$this->assertEquals($expected, $messages[0]->message);
}
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\migrate\MigrateExecutable;
use Drupal\node\Entity\NodeType;
/**
* Tests migrating non-Drupal translated content.
*
* Ensure it's possible to migrate in translations, even if there's no nid or
* tnid property on the source.
*
* @group migrate
*/
class MigrateExternalTranslatedTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'user',
'language',
'node',
'field',
'migrate_external_translated_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installSchema('node', ['node_access']);
$this->installEntitySchema('user');
$this->installEntitySchema('node');
// Create some languages.
ConfigurableLanguage::createFromLangcode('en')->save();
ConfigurableLanguage::createFromLangcode('fr')->save();
ConfigurableLanguage::createFromLangcode('es')->save();
// Create a content type.
NodeType::create([
'type' => 'external_test',
'name' => 'Test node type',
])->save();
}
/**
* Tests importing and rolling back our data.
*/
public function testMigrations(): void {
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
$storage = $this->container->get('entity_type.manager')->getStorage('node');
$this->assertCount(0, $storage->loadMultiple());
// Run the migrations.
$migration_ids = ['external_translated_test_node', 'external_translated_test_node_translation'];
$this->executeMigrations($migration_ids);
$this->assertCount(3, $storage->loadMultiple());
$node = $storage->load(1);
$this->assertEquals('en', $node->language()->getId());
$this->assertEquals('Cat', $node->title->value);
$this->assertEquals('Chat', $node->getTranslation('fr')->title->value);
$this->assertEquals('es - Cat', $node->getTranslation('es')->title->value);
$node = $storage->load(2);
$this->assertEquals('en', $node->language()->getId());
$this->assertEquals('Dog', $node->title->value);
$this->assertEquals('fr - Dog', $node->getTranslation('fr')->title->value);
$this->assertFalse($node->hasTranslation('es'), "No spanish translation for node 2");
$node = $storage->load(3);
$this->assertEquals('en', $node->language()->getId());
$this->assertEquals('Monkey', $node->title->value);
$this->assertFalse($node->hasTranslation('fr'), "No french translation for node 3");
$this->assertFalse($node->hasTranslation('es'), "No spanish translation for node 3");
$this->assertNull($storage->load(4), "No node 4 migrated");
// Roll back the migrations.
foreach ($migration_ids as $migration_id) {
$migration = $this->getMigration($migration_id);
$executable = new MigrateExecutable($migration, $this);
$executable->rollback();
}
$this->assertCount(0, $storage->loadMultiple());
}
}

View File

@@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\Event\MigratePostRowSaveEvent;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\MigrateExecutable;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests interruptions triggered during migrations.
*
* @group migrate
*/
class MigrateInterruptionTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['migrate', 'migrate_events_test'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::POST_ROW_SAVE,
[$this, 'postRowSaveEventRecorder']);
}
/**
* Tests migration interruptions.
*/
public function testMigrateEvents(): void {
// Run a simple little migration, which should trigger one of each event
// other than map_delete.
$definition = [
'migration_tags' => ['Interruption test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['data' => 'dummy value'],
['data' => 'dummy value2'],
],
'ids' => [
'data' => ['type' => 'string'],
],
],
'process' => ['value' => 'data'],
'destination' => ['plugin' => 'dummy'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
// When the import runs, the first row imported will trigger an
// interruption.
$result = $executable->import();
$this->assertEquals(MigrationInterface::RESULT_INCOMPLETE, $result);
// The status should have been reset to IDLE.
$this->assertEquals(MigrationInterface::STATUS_IDLE, $migration->getStatus());
}
/**
* Reacts to post-row-save event.
*
* @param \Drupal\migrate\Event\MigratePostRowSaveEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function postRowSaveEventRecorder(MigratePostRowSaveEvent $event, $name) {
$event->getMigration()->interruptMigration(MigrationInterface::RESULT_INCOMPLETE);
}
}

View File

@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateException;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Tests the Migrate Lookup service.
*
* @group migrate
*/
class MigrateLookupTest extends MigrateTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'node',
'field',
'user',
'text',
'migrate_lookup_test',
];
/**
* The migration lookup service.
*
* @var \Drupal\migrate\MigrateLookupInterface
*/
protected $migrateLookup;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setTestLogger();
$this->migrateLookup = $this->container->get('migrate.lookup');
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installConfig(['node', 'user']);
$this->createContentType(['type' => 'node_lookup']);
}
/**
* Tests scenarios around single id lookups.
*/
public function testSingleLookup(): void {
$this->executeMigration('sample_lookup_migration');
// Test numerically indexed source id.
$result = $this->migrateLookup->lookup('sample_lookup_migration', [17]);
$this->assertSame('1', $result[0]['nid']);
// Test associatively indexed source id.
$result = $this->migrateLookup->lookup('sample_lookup_migration', ['id' => 25]);
$this->assertSame('2', $result[0]['nid']);
// Test lookup not found.
$result = $this->migrateLookup->lookup('sample_lookup_migration', [1337]);
$this->assertSame([], $result);
}
/**
* Tests an invalid lookup.
*/
public function testInvalidIdLookup(): void {
$this->executeMigration('sample_lookup_migration');
$this->expectException(MigrateException::class);
$this->expectExceptionMessage("Extra unknown items for map migrate_map_sample_lookup_migration in source IDs: array (\n 'invalid_id' => 25,\n)");
// Test invalidly indexed source id.
$this->migrateLookup->lookup('sample_lookup_migration', ['invalid_id' => 25]);
}
/**
* Tests lookups with multiple source ids.
*/
public function testMultipleSourceIds(): void {
$this->executeMigration('sample_lookup_migration_multiple_source_ids');
// Test with full set of numerically indexed source ids.
$result = $this->migrateLookup->lookup('sample_lookup_migration_multiple_source_ids', [
25,
26,
]);
$this->assertCount(1, $result);
$this->assertSame('3', $result[0]['nid']);
// Test with full set of associatively indexed source ids.
$result = $this->migrateLookup->lookup('sample_lookup_migration_multiple_source_ids', [
'id' => 17,
'version_id' => 17,
]);
$this->assertCount(1, $result);
$this->assertSame('1', $result[0]['nid']);
// Test with full set of associatively indexed source ids in the wrong
// order.
$result = $this->migrateLookup->lookup('sample_lookup_migration_multiple_source_ids', [
'version_id' => 26,
'id' => 25,
]);
$this->assertCount(1, $result);
$this->assertSame('3', $result[0]['nid']);
// Test with a partial set of numerically indexed ids.
$result = $this->migrateLookup->lookup('sample_lookup_migration_multiple_source_ids', [25]);
$this->assertCount(2, $result);
$this->assertSame('2', $result[0]['nid']);
$this->assertSame('3', $result[1]['nid']);
// Test with a partial set of associatively indexed ids.
$result = $this->migrateLookup->lookup('sample_lookup_migration_multiple_source_ids', ['version_id' => 25]);
$this->assertCount(1, $result);
$this->assertSame('2', $result[0]['nid']);
}
/**
* Tests looking up against multiple migrations at once.
*
* @throws \Drupal\Component\Plugin\Exception\PluginException
* @throws \Drupal\migrate\MigrateException
*/
public function testMultipleMigrationLookup(): void {
$migrations = [
'sample_lookup_migration',
'sample_lookup_migration_2',
];
foreach ($migrations as $migration) {
$this->executeMigration($migration);
}
// Test numerically indexed source id.
$result = $this->migrateLookup->lookup($migrations, [17]);
$this->assertSame('1', $result[0]['nid']);
// Test associatively indexed source id.
$result = $this->migrateLookup->lookup($migrations, ['id' => 35]);
$this->assertSame('4', $result[0]['nid']);
// Test lookup not found.
$result = $this->migrateLookup->lookup($migrations, [1337]);
$this->assertSame([], $result);
}
/**
* Tests a lookup with string source ids.
*/
public function testLookupWithStringIds(): void {
$this->executeMigration('sample_lookup_migration_string_ids');
// Test numerically indexed source id.
$result = $this->migrateLookup->lookup('sample_lookup_migration_string_ids', ['node1']);
$this->assertSame('10', $result[0]['nid']);
// Test associatively indexed source id.
$result = $this->migrateLookup->lookup('sample_lookup_migration_string_ids', ['id' => 'node2']);
$this->assertSame('11', $result[0]['nid']);
// Test lookup not found.
$result = $this->migrateLookup->lookup('sample_lookup_migration_string_ids', ['node1337']);
$this->assertSame([], $result);
}
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Event\MigrateEvents;
use Drupal\migrate\Event\MigrateIdMapMessageEvent;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Plugin\migrate\id_map\Sql;
/**
* Tests whether idmap messages are sent to message interface when requested.
*
* @group migrate
*/
class MigrateMessageTest extends KernelTestBase implements MigrateMessageInterface {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['migrate', 'system'];
/**
* Migration to run.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* Messages accumulated during the migration run.
*
* @var array
*/
protected $messages = [];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installConfig(['system']);
// A simple migration, which will generate a message to the ID map because
// the concat plugin throws an exception if its source is not an array.
$definition = [
'migration_tags' => ['Message test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['name' => 'source_message', 'value' => 'a message'],
],
'ids' => [
'name' => ['type' => 'string'],
],
],
'process' => [
'message' => [
'plugin' => 'concat',
'source' => 'value',
],
],
'destination' => [
'plugin' => 'config',
'config_name' => 'system.maintenance',
],
];
$this->migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
}
/**
* Tests migration interruptions.
*/
public function testMessagesNotTeed(): void {
// We don't ask for messages to be teed, so don't expect any.
$executable = new MigrateExecutable($this->migration, $this);
$executable->import();
$this->assertCount(0, $this->messages);
}
/**
* Tests migration interruptions.
*/
public function testMessagesTeed(): void {
// Ask to receive any messages sent to the idmap.
\Drupal::service('event_dispatcher')->addListener(MigrateEvents::IDMAP_MESSAGE,
[$this, 'mapMessageRecorder']);
$executable = new MigrateExecutable($this->migration, $this);
$executable->import();
$this->assertCount(1, $this->messages);
$id = $this->migration->getPluginId();
$this->assertSame("source_message: $id:message:concat: 'a message' is not an array", reset($this->messages));
}
/**
* Tests the return value of getMessages().
*
* This method returns an iterator of StdClass objects. Check that these
* objects have the expected keys.
*/
public function testGetMessages(): void {
$id = $this->migration->getPluginId();
$expected_message = (object) [
'src_name' => 'source_message',
'dest_config_name' => NULL,
'msgid' => '1',
Sql::SOURCE_IDS_HASH => '170cde81762e22552d1b1578cf3804c89afefe9efbc7cc835185d7141060b032',
'level' => '1',
'message' => "$id:message:concat: 'a message' is not an array",
];
$executable = new MigrateExecutable($this->migration, $this);
$executable->import();
$count = 0;
foreach ($this->migration->getIdMap()->getMessages() as $message) {
++$count;
$this->assertEquals($expected_message, $message);
}
$this->assertEquals(1, $count);
}
/**
* Reacts to map message event.
*
* @param \Drupal\migrate\Event\MigrateIdMapMessageEvent $event
* The migration event.
* @param string $name
* The event name.
*/
public function mapMessageRecorder(MigrateIdMapMessageEvent $event, $name) {
if ($event->getLevel() == MigrationInterface::MESSAGE_NOTICE ||
$event->getLevel() == MigrationInterface::MESSAGE_INFORMATIONAL) {
$type = 'status';
}
else {
$type = 'error';
}
$source_id_string = implode(',', $event->getSourceIdValues());
$this->display($source_id_string . ': ' . $event->getMessage(), $type);
}
/**
* {@inheritdoc}
*/
public function display($message, $type = 'status') {
$this->messages[] = $message;
}
}

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\Core\Database\Database;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests that a SQL migration can be instantiated without a database connection.
*
* @group migrate
*/
class MigrateMissingDatabaseTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'migrate_missing_database_test'];
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManager
*/
protected $migrationPluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migrationPluginManager = \Drupal::service('plugin.manager.migration');
// Set the 'migrate' database connection to use a missing database.
$info = Database::getConnectionInfo('default')['default'];
$info['database'] = 'godot';
Database::addConnectionInfo('migrate', 'default', $info);
}
/**
* Tests a SQL migration without the database connection.
*
* - The migration can be instantiated.
* - The checkRequirements() method throws a RequirementsException.
*/
public function testMissingDatabase(): void {
if (Database::getConnection()->driver() === 'sqlite') {
$this->markTestSkipped('Not compatible with sqlite');
}
$migration = $this->migrationPluginManager->createInstance('missing_database');
$this->assertInstanceOf(MigrationInterface::class, $migration);
$this->assertInstanceOf(MigrateIdMapInterface::class, $migration->getIdMap());
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage('No database connection available for source plugin migrate_missing_database_test');
$migration->checkRequirements();
}
}

View File

@@ -0,0 +1,173 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\migrate\process\Get;
use Drupal\migrate\Plugin\migrate\process\SubProcess;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigratePluginManagerInterface;
use Drupal\migrate\Plugin\MigrateProcessInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
/**
* Tests the format of messages from process plugin exceptions.
*
* @group migrate
*/
class MigrateProcessErrorMessagesTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'migrate_events_test',
'migrate',
];
/**
* A prophesized Process Plugin Manager.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected ObjectProphecy $processPluginManager;
/**
* A prophesized ID Map Plugin Manager.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected ObjectProphecy $idMapPluginManager;
/**
* A prophesized ID Map.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected ObjectProphecy $idMap;
/**
* The default stub migration definition.
*
* @var array
*/
protected array $definition = [
'id' => 'process_errors_migration',
'idMap' => [
'plugin' => 'idmap_prophecy',
],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
[
'id' => 1,
'my_property' => [
'subfield' => [
42,
],
],
],
],
'ids' => ['id' => ['type' => 'integer']],
],
'process' => [
'id' => 'id',
],
'destination' => [
'plugin' => 'dummy',
],
'migration_dependencies' => [],
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->processPluginManager = $this->prophesize(MigratePluginManagerInterface::class);
$this->idMapPluginManager = $this->prophesize(MigratePluginManagerInterface::class);
$this->idMap = $this->prophesize(MigrateIdMapInterface::class);
}
/**
* Tests format of map messages saved from plugin exceptions.
*/
public function testProcessErrorMessage(): void {
$this->definition['process']['error']['plugin'] = 'test_error';
$this->idMap->saveMessage(['id' => 1], "process_errors_migration:error:test_error: Process exception.", MigrationInterface::MESSAGE_ERROR)->shouldBeCalled();
$this->setPluginManagers();
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($this->definition);
$executable = new MigrateExecutable($migration, $this);
$executable->import();
}
/**
* Tests format of map messages saved from sub_process exceptions.
*
* This checks the format of messages that are thrown from normal process
* plugins while being executed inside a sub_process pipeline as they
* bubble up to the main migration.
*/
public function testSubProcessErrorMessage(): void {
$this->definition['process']['subprocess_error'] = [
'plugin' => 'sub_process',
'source' => 'my_property',
'process' => [
'subfield' => [
[
'plugin' => 'test_error',
'value' => 'subfield',
],
],
],
];
$this->processPluginManager->createInstance('sub_process', Argument::cetera())
->will(fn($x) => new SubProcess($x[1], 'sub_process', ['handle_multiples' => TRUE]));
$this->idMap->saveMessage(['id' => 1], "process_errors_migration:subprocess_error:sub_process: test_error: Process exception.", MigrationInterface::MESSAGE_ERROR)->shouldBeCalled();
$this->setPluginManagers();
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($this->definition);
$executable = new MigrateExecutable($migration, $this);
$executable->import();
}
/**
* Prepares and sets the prophesized plugin managers.
*/
protected function setPluginManagers() {
$error_plugin_prophecy = $this->prophesize(MigrateProcessInterface::class);
$error_plugin_prophecy->getPluginDefinition()->willReturn(['plugin_id' => 'test_error']);
$error_plugin_prophecy->getPluginId()->willReturn('test_error');
$error_plugin_prophecy->reset()->shouldBeCalled();
$error_plugin_prophecy->transform(Argument::cetera())->willThrow(new MigrateException('Process exception.'));
$this->processPluginManager->createInstance('get', Argument::cetera())
->will(fn($x) => new Get($x[1], 'get', ['handle_multiples' => TRUE]));
$this->processPluginManager->createInstance('test_error', Argument::cetera())->willReturn($error_plugin_prophecy->reveal());
$this->idMap->setMessage(Argument::any())->willReturn();
$this->idMap->getRowBySource(Argument::any())->willReturn([]);
$this->idMap->delete(Argument::cetera())->willReturn();
$this->idMap->saveIdMapping(Argument::cetera())->willReturn();
$this->idMapPluginManager->createInstance('idmap_prophecy', Argument::cetera())->willReturn($this->idMap->reveal());
$this->container->set('plugin.manager.migrate.process', $this->processPluginManager->reveal());
$this->container->set('plugin.manager.migrate.id_map', $this->idMapPluginManager->reveal());
}
}

View File

@@ -0,0 +1,178 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateExecutable;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Tests rolling back of imports.
*
* @group migrate
*/
class MigrateRollbackEntityConfigTest extends MigrateTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = [
'field',
'taxonomy',
'text',
'language',
'config_translation',
'user',
'system',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('taxonomy_term');
$this->installConfig(['taxonomy']);
}
/**
* Tests rolling back configuration entity translations.
*/
public function testConfigEntityRollback(): void {
// We use vocabularies to demonstrate importing and rolling back
// configuration entities with translations. First, import vocabularies.
$vocabulary_data_rows = [
['id' => '1', 'name' => 'categories', 'weight' => '2'],
['id' => '2', 'name' => 'tags', 'weight' => '1'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'vocabularies',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $vocabulary_data_rows,
'ids' => $ids,
],
'process' => [
'vid' => 'id',
'name' => 'name',
'weight' => 'weight',
],
'destination' => ['plugin' => 'entity:taxonomy_vocabulary'],
];
/** @var \Drupal\migrate\Plugin\Migration $vocabulary_migration */
$vocabulary_migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$vocabulary_id_map = $vocabulary_migration->getIdMap();
$this->assertTrue($vocabulary_migration->getDestinationPlugin()
->supportsRollback());
// Import and validate vocabulary config entities were created.
$vocabulary_executable = new MigrateExecutable($vocabulary_migration, $this);
$vocabulary_executable->import();
foreach ($vocabulary_data_rows as $row) {
/** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
$vocabulary = Vocabulary::load($row['id']);
$this->assertNotEmpty($vocabulary);
$map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]);
$this->assertNotNull($map_row['destid1']);
}
// Second, import translations of the vocabulary name property.
$vocabulary_i18n_data_rows = [
[
'id' => '1',
'name' => '1',
'language' => 'fr',
'property' => 'name',
'translation' => 'fr - categories',
],
[
'id' => '2',
'name' => '2',
'language' => 'fr',
'property' => 'name',
'translation' => 'fr - tags',
],
];
$ids = [
'id' => ['type' => 'integer'],
'language' => ['type' => 'string'],
];
$definition = [
'id' => 'i18n_vocabularies',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $vocabulary_i18n_data_rows,
'ids' => $ids,
'constants' => [
'name' => 'name',
],
],
'process' => [
'vid' => 'id',
'langcode' => 'language',
'property' => 'constants/name',
'translation' => 'translation',
],
'destination' => [
'plugin' => 'entity:taxonomy_vocabulary',
'translations' => 'true',
],
];
$vocabulary_i18n__migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$vocabulary_i18n_id_map = $vocabulary_i18n__migration->getIdMap();
$this->assertTrue($vocabulary_i18n__migration->getDestinationPlugin()
->supportsRollback());
// Import and validate vocabulary config entities were created.
$vocabulary_i18n_executable = new MigrateExecutable($vocabulary_i18n__migration, $this);
$vocabulary_i18n_executable->import();
$language_manager = \Drupal::service('language_manager');
foreach ($vocabulary_i18n_data_rows as $row) {
$langcode = $row['language'];
$id = 'taxonomy.vocabulary.' . $row['id'];
/** @var \Drupal\language\Config\LanguageConfigOverride $config_translation */
$config_translation = $language_manager->getLanguageConfigOverride($langcode, $id);
$this->assertSame($row['translation'], $config_translation->get('name'));
$map_row = $vocabulary_i18n_id_map->getRowBySource(['id' => $row['id'], 'language' => $row['language']]);
$this->assertNotNull($map_row['destid1']);
}
// Perform the rollback and confirm the translation was deleted and the map
// table row removed.
$vocabulary_i18n_executable->rollback();
foreach ($vocabulary_i18n_data_rows as $row) {
$langcode = $row['language'];
$id = 'taxonomy.vocabulary.' . $row['id'];
/** @var \Drupal\language\Config\LanguageConfigOverride $config_translation */
$config_translation = $language_manager->getLanguageConfigOverride($langcode, $id);
$this->assertNull($config_translation->get('name'));
$map_row = $vocabulary_i18n_id_map->getRowBySource(['id' => $row['id'], 'language' => $row['language']]);
$this->assertFalse($map_row);
}
// Confirm the original vocabulary still exists.
foreach ($vocabulary_data_rows as $row) {
/** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
$vocabulary = Vocabulary::load($row['id']);
$this->assertNotEmpty($vocabulary);
$map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]);
$this->assertNotNull($map_row['destid1']);
}
}
}

View File

@@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Row;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
/**
* Tests rolling back of imports.
*
* @group migrate
*/
class MigrateRollbackTest extends MigrateTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['field', 'taxonomy', 'text', 'user', 'system'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
$this->installEntitySchema('taxonomy_term');
$this->installConfig(['taxonomy']);
}
/**
* Tests rolling back configuration and content entities.
*/
public function testRollback(): void {
// We use vocabularies to demonstrate importing and rolling back
// configuration entities.
$vocabulary_data_rows = [
['id' => '1', 'name' => 'categories', 'weight' => '2'],
['id' => '2', 'name' => 'tags', 'weight' => '1'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'vocabularies',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $vocabulary_data_rows,
'ids' => $ids,
],
'process' => [
'vid' => 'id',
'name' => 'name',
'weight' => 'weight',
],
'destination' => ['plugin' => 'entity:taxonomy_vocabulary'],
];
$vocabulary_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$vocabulary_id_map = $vocabulary_migration->getIdMap();
$this->assertTrue($vocabulary_migration->getDestinationPlugin()->supportsRollback());
// Import and validate vocabulary config entities were created.
$vocabulary_executable = new MigrateExecutable($vocabulary_migration, $this);
$vocabulary_executable->import();
foreach ($vocabulary_data_rows as $row) {
/** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
$vocabulary = Vocabulary::load($row['id']);
$this->assertNotEmpty($vocabulary);
$map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]);
$this->assertNotNull($map_row['destid1']);
}
// We use taxonomy terms to demonstrate importing and rolling back content
// entities.
$term_data_rows = [
['id' => '1', 'vocab' => '1', 'name' => 'music'],
['id' => '2', 'vocab' => '2', 'name' => 'Bach'],
['id' => '3', 'vocab' => '2', 'name' => 'Beethoven'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'terms',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $term_data_rows,
'ids' => $ids,
],
'process' => [
'tid' => 'id',
'vid' => 'vocab',
'name' => 'name',
],
'destination' => ['plugin' => 'entity:taxonomy_term'],
'migration_dependencies' => ['required' => ['vocabularies']],
];
$term_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$term_id_map = $term_migration->getIdMap();
$this->assertTrue($term_migration->getDestinationPlugin()->supportsRollback());
// Pre-create a term, to make sure it isn't deleted on rollback.
$preserved_term_ids[] = 1;
$new_term = Term::create(['tid' => 1, 'vid' => 1, 'name' => 'music']);
$new_term->save();
// Import and validate term entities were created.
$term_executable = new MigrateExecutable($term_migration, $this);
$term_executable->import();
// Also explicitly mark one row to be preserved on rollback.
$preserved_term_ids[] = 2;
$map_row = $term_id_map->getRowBySource(['id' => 2]);
$dummy_row = new Row(['id' => 2], $ids);
$term_id_map->saveIdMapping($dummy_row, [$map_row['destid1']],
$map_row['source_row_status'], MigrateIdMapInterface::ROLLBACK_PRESERVE);
foreach ($term_data_rows as $row) {
/** @var \Drupal\taxonomy\Entity\Term $term */
$term = Term::load($row['id']);
$this->assertNotEmpty($term);
$map_row = $term_id_map->getRowBySource(['id' => $row['id']]);
$this->assertNotNull($map_row['destid1']);
}
// Add a failed row to test if this can be rolled back without errors.
$this->mockFailure($term_migration, ['id' => '4', 'vocab' => '2', 'name' => 'FAIL']);
// Rollback and verify the entities are gone.
$term_executable->rollback();
foreach ($term_data_rows as $row) {
$term = Term::load($row['id']);
if (in_array($row['id'], $preserved_term_ids)) {
$this->assertNotNull($term);
}
else {
$this->assertNull($term);
}
$map_row = $term_id_map->getRowBySource(['id' => $row['id']]);
$this->assertFalse($map_row);
}
$vocabulary_executable->rollback();
foreach ($vocabulary_data_rows as $row) {
$term = Vocabulary::load($row['id']);
$this->assertNull($term);
$map_row = $vocabulary_id_map->getRowBySource(['id' => $row['id']]);
$this->assertFalse($map_row);
}
// Test that simple configuration is not rollbackable.
$term_setting_rows = [
['id' => 1, 'override_selector' => '0', 'terms_per_page_admin' => '10'],
];
$ids = ['id' => ['type' => 'integer']];
$definition = [
'id' => 'taxonomy_settings',
'migration_tags' => ['Import and rollback test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => $term_setting_rows,
'ids' => $ids,
],
'process' => [
'override_selector' => 'override_selector',
'terms_per_page_admin' => 'terms_per_page_admin',
],
'destination' => [
'plugin' => 'config',
'config_name' => 'taxonomy.settings',
],
'migration_dependencies' => ['required' => ['vocabularies']],
];
$settings_migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$this->assertFalse($settings_migration->getDestinationPlugin()->supportsRollback());
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
/**
* Tests row skips triggered during hook_migrate_prepare_row().
*
* @group migrate
*/
class MigrateSkipRowTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
protected static $modules = ['migrate', 'migrate_prepare_row_test'];
/**
* Tests migration interruptions.
*/
public function testPrepareRowSkip(): void {
// Run a simple little migration with two data rows which should be skipped
// in different ways.
$definition = [
'migration_tags' => ['prepare_row test'],
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['id' => '1', 'data' => 'skip_and_record'],
['id' => '2', 'data' => 'skip_and_do_not_record'],
],
'ids' => [
'id' => ['type' => 'string'],
],
],
'process' => ['value' => 'data'],
'destination' => [
'plugin' => 'config',
'config_name' => 'migrate_test.settings',
],
'load' => ['plugin' => 'null'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$result = $executable->import();
$this->assertEquals(MigrationInterface::RESULT_COMPLETED, $result);
/** @var \Drupal\migrate\Plugin\MigrateIdMapInterface $id_map_plugin */
$id_map_plugin = $migration->getIdMap();
// The first row is recorded in the map as ignored.
$map_row = $id_map_plugin->getRowBySource(['id' => 1]);
$this->assertEquals(MigrateIdMapInterface::STATUS_IGNORED, $map_row['source_row_status']);
// Check that no message has been logged for the first exception.
$messages = $id_map_plugin->getMessages(['id' => 1])->fetchAll();
$this->assertEmpty($messages);
// The second row is not recorded in the map.
$map_row = $id_map_plugin->getRowBySource(['id' => 2]);
$this->assertFalse($map_row);
// Check that the correct message has been logged for the second exception.
$messages = $id_map_plugin->getMessages(['id' => 2])->fetchAll();
$this->assertCount(1, $messages);
$message = reset($messages);
$this->assertEquals('skip_and_do_not_record message', $message->message);
$this->assertEquals(MigrationInterface::MESSAGE_INFORMATIONAL, $message->level);
// Insert a custom processor in the process flow.
$definition['process']['value'] = [
'source' => 'data',
'plugin' => 'test_skip_row_process',
];
// Change data to avoid triggering again hook_migrate_prepare_row().
$definition['source']['data_rows'] = [
['id' => '1', 'data' => 'skip_and_record (use plugin)'],
['id' => '2', 'data' => 'skip_and_do_not_record (use plugin)'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
$executable = new MigrateExecutable($migration);
$result = $executable->import();
$this->assertEquals(MigrationInterface::RESULT_COMPLETED, $result);
$id_map_plugin = $migration->getIdMap();
// The first row is recorded in the map as ignored.
$map_row = $id_map_plugin->getRowBySource(['id' => 1]);
$this->assertEquals(MigrateIdMapInterface::STATUS_IGNORED, $map_row['source_row_status']);
// The second row is not recorded in the map.
$map_row = $id_map_plugin->getRowBySource(['id' => 2]);
$this->assertFalse($map_row);
}
}

View File

@@ -0,0 +1,209 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
/**
* Base class for tests of Migrate source plugins.
*
* Implementing classes must declare a providerSource() method for this class
* to work, defined as follows:
*
* @code
* abstract public static function providerSource(): array;
* @endcode
*
* The returned array should be as follows:
*
* @code
* Array of data sets to test, each of which is a numerically indexed array
* with the following elements:
* - An array of source data, which can be optionally processed and set up
* by subclasses.
* - An array of expected result rows.
* - (optional) The number of result rows the plugin under test is expected
* to return. If this is not a numeric value, the plugin will not be
* counted.
* - (optional) Array of configuration options for the plugin under test.
* @endcode
*
* @see \Drupal\Tests\migrate\Kernel\MigrateSourceTestBase::testSource
*/
abstract class MigrateSourceTestBase extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'migrate_skip_all_rows_test'];
/**
* The mocked migration.
*
* @var \Drupal\migrate\Plugin\MigrationInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $migration;
/**
* The source plugin under test.
*
* @var \Drupal\migrate\Plugin\MigrateSourceInterface
*/
protected $plugin;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a mock migration. This will be injected into the source plugin
// under test.
$this->migration = $this->prophesize(MigrationInterface::class);
$this->migration->id()->willReturn(
$this->randomMachineName(16)
);
// Prophesize a useless ID map plugin and an empty set of destination IDs.
// Calling code can override these prophecies later and set up different
// behaviors.
$this->migration->getIdMap()->willReturn(
$this->prophesize(MigrateIdMapInterface::class)->reveal()
);
$this->migration->getDestinationIds()->willReturn([]);
}
/**
* Determines the plugin to be tested by reading the class @covers annotation.
*
* @return string
*/
protected function getPluginClass() {
$covers = $this->getTestClassCovers();
if (!empty($covers)) {
return $covers[0];
}
else {
$this->fail('No plugin class was specified');
}
}
/**
* Instantiates the source plugin under test.
*
* @param array $configuration
* The source plugin configuration.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface|object
* The fully configured source plugin.
*/
protected function getPlugin(array $configuration) {
// Only create the plugin once per test.
if ($this->plugin) {
return $this->plugin;
}
$class = ltrim($this->getPluginClass(), '\\');
/** @var \Drupal\migrate\Plugin\MigratePluginManager $plugin_manager */
$plugin_manager = $this->container->get('plugin.manager.migrate.source');
foreach ($plugin_manager->getDefinitions() as $id => $definition) {
if (ltrim($definition['class'], '\\') == $class) {
$this->plugin = $plugin_manager
->createInstance($id, $configuration, $this->migration->reveal());
$this->migration
->getSourcePlugin()
->willReturn($this->plugin);
return $this->plugin;
}
}
$this->fail('No plugin found for class ' . $class);
}
/**
* Tests the source plugin against a particular data set.
*
* @param array $source_data
* The source data that the source plugin will read.
* @param array $expected_data
* The result rows the source plugin is expected to return.
* @param mixed $expected_count
* (optional) How many rows the source plugin is expected to return.
* Defaults to count($expected_data). If set to a non-null, non-numeric
* value (like FALSE or 'nope'), the source plugin will not be counted.
* @param array $configuration
* (optional) Configuration for the source plugin.
* @param mixed $high_water
* (optional) The value of the high water field.
*
* @dataProvider providerSource
*/
public function testSource(array $source_data, array $expected_data, $expected_count = NULL, array $configuration = [], $high_water = NULL): void {
$plugin = $this->getPlugin($configuration);
$clone_plugin = clone $plugin;
// All source plugins must define IDs.
$this->assertNotEmpty($plugin->getIds());
// If there is a high water mark, set it in the high water storage.
if (isset($high_water)) {
$this->container
->get('keyvalue')
->get('migrate:high_water')
->set($this->migration->reveal()->id(), $high_water);
}
if (is_null($expected_count)) {
$expected_count = count($expected_data);
}
// If an expected count was given, assert it only if the plugin is
// countable.
if (is_numeric($expected_count)) {
$this->assertInstanceOf('\Countable', $plugin);
$this->assertCount($expected_count, $plugin);
}
$i = 0;
/** @var \Drupal\migrate\Row $row */
foreach ($plugin as $row) {
$this->assertInstanceOf(Row::class, $row);
$expected = $expected_data[$i++];
$actual = $row->getSource();
foreach ($expected as $key => $value) {
$this->assertArrayHasKey($key, $actual);
$msg = sprintf("Value at 'array[%s][%s]' is not correct.", $i - 1, $key);
if (is_array($value)) {
ksort($value);
ksort($actual[$key]);
$this->assertEquals($value, $actual[$key], $msg);
}
else {
$this->assertEquals((string) $value, (string) $actual[$key], $msg);
}
}
}
// False positives occur if the foreach is not entered. So, confirm the
// foreach loop was entered if the expected count is greater than 0.
if ($expected_count > 0) {
$this->assertGreaterThan(0, $i);
// Test that we can skip all rows.
\Drupal::state()->set('migrate_skip_all_rows_test_migrate_prepare_row', TRUE);
foreach ($clone_plugin as $row) {
$this->fail('Row not skipped');
}
}
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\Core\Cache\MemoryCounterBackendFactory;
use Drupal\sqlite\Driver\Database\sqlite\Connection;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
/**
* Base class for tests of Migrate source plugins that use a database.
*/
abstract class MigrateSqlSourceTestBase extends MigrateSourceTestBase {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
parent::register($container);
$container
->register('cache_factory', MemoryCounterBackendFactory::class)
->addArgument(new Reference('datetime.time'));
}
/**
* Builds an in-memory SQLite database from a set of source data.
*
* @param array $source_data
* The source data, keyed by table name. Each table is an array containing
* the rows in that table.
*
* @return \Drupal\sqlite\Driver\Database\sqlite\Connection
* The SQLite database connection.
*/
protected function getDatabase(array $source_data) {
// Create an in-memory SQLite database. Plugins can interact with it like
// any other database, and it will cease to exist when the connection is
// closed.
$connection_options = ['database' => ':memory:'];
$pdo = Connection::open($connection_options);
$connection = new Connection($pdo, $connection_options);
// Create the tables and fill them with data.
foreach ($source_data as $table => $rows) {
// Use the biggest row to build the table schema.
$counts = array_map('count', $rows);
asort($counts);
$pilot = $rows[array_key_last($counts)];
$connection->schema()
->createTable($table, [
// SQLite uses loose affinity typing, so it's OK for every field to
// be a text field.
'fields' => array_map(function () {
return ['type' => 'text'];
}, $pilot),
]);
$fields = array_keys($pilot);
$insert = $connection->insert($table)->fields($fields);
array_walk($rows, [$insert, 'values']);
$insert->execute();
}
return $connection;
}
/**
* Tests the source plugin against a particular data set.
*
* @param array $source_data
* The source data that the plugin will read. See getDatabase() for the
* expected format.
* @param array $expected_data
* The result rows the plugin is expected to return.
* @param int $expected_count
* (optional) How many rows the source plugin is expected to return.
* @param array $configuration
* (optional) Configuration for the source plugin.
* @param mixed $high_water
* (optional) The value of the high water field.
* @param string|null $expected_cache_key
* (optional) The expected cache key.
*
* @dataProvider providerSource
*
* @requires extension pdo_sqlite
*/
public function testSource(array $source_data, array $expected_data, $expected_count = NULL, array $configuration = [], $high_water = NULL, $expected_cache_key = NULL): void {
$plugin = $this->getPlugin($configuration);
// Since we don't yet inject the database connection, we need to use a
// reflection hack to set it in the plugin instance.
$reflector = new \ReflectionObject($plugin);
$property = $reflector->getProperty('database');
$property->setValue($plugin, $this->getDatabase($source_data));
/** @var MemoryCounterBackend $cache **/
$cache = \Drupal::cache('migrate');
if ($expected_cache_key) {
// Verify the computed cache key.
$property = $reflector->getProperty('cacheKey');
$this->assertSame($expected_cache_key, $property->getValue($plugin));
// Cache miss prior to calling ::count().
$this->assertFalse($cache->get($expected_cache_key, 'cache'));
$this->assertSame([], $cache->getCounter('set'));
$count = $plugin->count();
$this->assertSame($expected_count, $count);
$this->assertSame([$expected_cache_key => 1], $cache->getCounter('set'));
// Cache hit afterwards.
$cache_item = $cache->get($expected_cache_key, 'cache');
$this->assertNotSame(FALSE, $cache_item, 'This is not a cache hit.');
$this->assertSame($expected_count, $cache_item->data);
}
else {
$this->assertSame([], $cache->getCounter('set'));
$plugin->count();
$this->assertSame([], $cache->getCounter('set'));
}
parent::testSource($source_data, $expected_data, $expected_count, $configuration, $high_water);
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests migration status tracking.
*
* @group migrate
*/
class MigrateStatusTest extends MigrateTestBase {
/**
* Tests different connection types.
*/
public function testStatus(): void {
// Create a minimally valid migration.
$definition = [
'id' => 'migrate_status_test',
'migration_tags' => ['Testing'],
'source' => ['plugin' => 'empty'],
'destination' => [
'plugin' => 'config',
'config_name' => 'migrate_test.settings',
],
'process' => ['foo' => 'bar'],
];
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration($definition);
// Default status is idle.
$status = $migration->getStatus();
$this->assertSame(MigrationInterface::STATUS_IDLE, $status);
// Test setting and retrieving all known status values.
$status_list = [
MigrationInterface::STATUS_IDLE,
MigrationInterface::STATUS_IMPORTING,
MigrationInterface::STATUS_ROLLING_BACK,
MigrationInterface::STATUS_STOPPING,
MigrationInterface::STATUS_DISABLED,
];
foreach ($status_list as $status) {
$migration->setStatus($status);
$this->assertSame($status, $migration->getStatus());
}
}
}

View File

@@ -0,0 +1,142 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\field\Entity\FieldConfig;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
/**
* Tests the migrate.stub Service.
*
* @group migrate
*/
class MigrateStubTest extends MigrateTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'system',
'node',
'field',
'user',
'text',
'filter',
'migrate_stub_test',
];
/**
* The migrate stub service.
*
* @var \Drupal\migrate\MigrateStubInterface
*/
protected $migrateStub;
/**
* The migration lookup service.
*
* @var \Drupal\migrate\MigrateLookupInterface
*/
protected $migrateLookup;
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->setTestLogger();
$this->migrateStub = $this->container->get('migrate.stub');
$this->migrateLookup = $this->container->get('migrate.lookup');
$this->migrationPluginManager = $this->container->get('plugin.manager.migration');
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installSchema('node', 'node_access');
$this->installConfig(['node', 'user']);
$this->createContentType(['type' => 'node_lookup']);
}
/**
* Tests stub creation.
*/
public function testCreateStub(): void {
$this->assertSame([], $this->migrateLookup->lookup('sample_stubbing_migration', [17]));
$ids = $this->migrateStub->createStub('sample_stubbing_migration', [17]);
$this->assertSame([$ids], $this->migrateLookup->lookup('sample_stubbing_migration', [17]));
$this->assertNotNull(\Drupal::entityTypeManager()->getStorage('node')->load($ids['nid']));
}
/**
* Tests raw stub creation.
*/
public function testCreateStubRawReturn(): void {
$this->assertSame([], $this->migrateLookup->lookup('sample_stubbing_migration', [17]));
$ids = $this->migrateStub->createStub('sample_stubbing_migration', [17], [], FALSE);
$this->assertSame($ids, [$this->migrateLookup->lookup('sample_stubbing_migration', [17])[0]['nid']]);
$this->assertNotNull(\Drupal::entityTypeManager()->getStorage('node')->load($ids[0]));
}
/**
* Tests stub creation with default values.
*/
public function testStubWithDefaultValues(): void {
$this->assertSame([], $this->migrateLookup->lookup('sample_stubbing_migration', [17]));
$ids = $this->migrateStub->createStub('sample_stubbing_migration', [17], ['title' => "Placeholder for source id 17"]);
$this->assertSame([$ids], $this->migrateLookup->lookup('sample_stubbing_migration', [17]));
$node = \Drupal::entityTypeManager()->getStorage('node')->load($ids['nid']);
$this->assertNotNull($node);
// Test that our default value was set as the node title.
$this->assertSame("Placeholder for source id 17", $node->label());
// Test that running the migration replaces the node title.
$this->executeMigration('sample_stubbing_migration');
$node = \Drupal::entityTypeManager()->getStorage('node')->loadUnchanged($ids['nid']);
$this->assertSame("Sample 1", $node->label());
}
/**
* Tests stub creation with bundle fields.
*/
public function testStubWithBundleFields(): void {
$this->createContentType(['type' => 'node_stub']);
// Make "Body" field required to make stubbing populate field value.
$body_field = FieldConfig::loadByName('node', 'node_stub', 'body');
$body_field->setRequired(TRUE)->save();
$this->assertSame([], $this->migrateLookup->lookup('sample_stubbing_migration', [33]));
$ids = $this->migrateStub->createStub('sample_stubbing_migration', [33], []);
$this->assertSame([$ids], $this->migrateLookup->lookup('sample_stubbing_migration', [33]));
$node = \Drupal::entityTypeManager()->getStorage('node')->load($ids['nid']);
$this->assertNotNull($node);
// Make sure the "Body" field value was populated.
$this->assertNotEmpty($node->get('body')->value);
}
/**
* Tests invalid source id count.
*/
public function testInvalidSourceIdCount(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('Expected and provided source id counts do not match.');
$this->migrateStub->createStub('sample_stubbing_migration_with_multiple_source_ids', [17]);
}
/**
* Tests invalid source ids keys.
*/
public function testInvalidSourceIdKeys(): void {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage("'version_id' is defined as a source ID but has no value.");
$this->migrateStub->createStub('sample_stubbing_migration_with_multiple_source_ids', ['id' => 17, 'not_a_key' => 17]);
}
}

View File

@@ -0,0 +1,270 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\Core\Database\Database;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\MigrateMessageInterface;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
/**
* Creates abstract base class for migration tests.
*/
abstract class MigrateTestBase extends KernelTestBase implements MigrateMessageInterface {
/**
* TRUE to collect messages instead of displaying them.
*
* @var bool
*/
protected $collectMessages = FALSE;
/**
* A two dimensional array of messages.
*
* The first key is the type of message, the second is just numeric. Values
* are the messages.
*
* @var array
*/
protected $migrateMessages;
/**
* The primary migration being tested.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* The source database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $sourceDatabase;
/**
* A logger prophecy object.
*
* Using ::setTestLogger(), this prophecy will be configured and injected into
* the container. Using $this->logger->function(args)->shouldHaveBeenCalled()
* you can assert that the logger was called.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected $logger;
protected static $modules = ['migrate'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->createMigrationConnection();
$this->sourceDatabase = Database::getConnection('default', 'migrate');
// Attach the original test prefix as a database, for SQLite to attach its
// database file.
$this->sourceDatabase->attachDatabase(substr($this->sourceDatabase->getConnectionOptions()['prefix'], 0, -1));
}
/**
* Changes the database connection to the prefixed one.
*
* @todo Remove when we don't use global. https://www.drupal.org/node/2552791
*/
private function createMigrationConnection() {
// If the backup already exists, something went terribly wrong.
// This case is possible, because database connection info is a static
// global state construct on the Database class, which at least persists
// for all test methods executed in one PHP process.
if (Database::getConnectionInfo('simpletest_original_migrate')) {
throw new \RuntimeException("Bad Database connection state: 'simpletest_original_migrate' connection key already exists. Broken test?");
}
// Clone the current connection and replace the current prefix.
$connection_info = Database::getConnectionInfo('migrate');
if ($connection_info) {
Database::renameConnection('migrate', 'simpletest_original_migrate');
}
$connection_info = Database::getConnectionInfo('default');
foreach ($connection_info as $target => $value) {
$prefix = $value['prefix'];
// Tests use 7 character prefixes at most so this can't cause collisions.
$connection_info[$target]['prefix'] = $prefix . '0';
}
Database::addConnectionInfo('migrate', 'default', $connection_info['default']);
}
/**
* {@inheritdoc}
*/
protected function tearDown(): void {
$this->cleanupMigrateConnection();
parent::tearDown();
$this->collectMessages = FALSE;
unset($this->migration, $this->migrateMessages);
}
/**
* Cleans up the test migrate connection.
*
* @todo Remove when we don't use global. https://www.drupal.org/node/2552791
*/
private function cleanupMigrateConnection() {
Database::removeConnection('migrate');
$original_connection_info = Database::getConnectionInfo('simpletest_original_migrate');
if ($original_connection_info) {
Database::renameConnection('simpletest_original_migrate', 'migrate');
}
}
/**
* Prepare any dependent migrations.
*
* @param array $id_mappings
* A list of ID mappings keyed by migration IDs. Each ID mapping is a list
* of two arrays, the first are source IDs and the second are destination
* IDs.
*/
protected function prepareMigrations(array $id_mappings) {
$manager = $this->container->get('plugin.manager.migration');
foreach ($id_mappings as $migration_id => $data) {
foreach ($manager->createInstances($migration_id) as $migration) {
$id_map = $migration->getIdMap();
$id_map->setMessage($this);
$source_ids = $migration->getSourcePlugin()->getIds();
foreach ($data as $id_mapping) {
$row = new Row(array_combine(array_keys($source_ids), $id_mapping[0]), $source_ids);
$id_map->saveIdMapping($row, $id_mapping[1]);
}
}
}
}
/**
* Modify a migration's configuration before executing it.
*
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to execute.
*/
protected function prepareMigration(MigrationInterface $migration) {
// Default implementation for test classes not requiring modification.
}
/**
* Executes a single migration.
*
* @param string|\Drupal\migrate\Plugin\MigrationInterface $migration
* The migration to execute, or its ID.
*/
protected function executeMigration($migration) {
if (is_string($migration)) {
$this->migration = $this->getMigration($migration);
}
else {
$this->migration = $migration;
}
if ($this instanceof MigrateDumpAlterInterface) {
static::migrateDumpAlter($this);
}
$this->prepareMigration($this->migration);
(new MigrateExecutable($this->migration, $this))->import();
}
/**
* Executes a set of migrations in dependency order.
*
* @param string[] $ids
* Array of migration IDs, in any order. If any of these migrations use a
* deriver, the derivatives will be made before execution.
*/
protected function executeMigrations(array $ids) {
$manager = $this->container->get('plugin.manager.migration');
$instances = $manager->createInstances($ids);
array_walk($instances, [$this, 'executeMigration']);
}
/**
* {@inheritdoc}
*/
public function display($message, $type = 'status') {
if ($this->collectMessages) {
$this->migrateMessages[$type][] = $message;
}
else {
$this->assertEquals('status', $type, $message);
}
}
/**
* Start collecting messages and erase previous messages.
*/
public function startCollectingMessages() {
$this->collectMessages = TRUE;
$this->migrateMessages = [];
}
/**
* Stop collecting messages.
*/
public function stopCollectingMessages() {
$this->collectMessages = FALSE;
}
/**
* Records a failure in the map table of a specific migration.
*
* This is done in order to test scenarios which require a failed row.
*
* @param string|\Drupal\migrate\Plugin\MigrationInterface $migration
* The migration entity, or its ID.
* @param array $row
* The raw source row which "failed".
* @param int $status
* (optional) The failure status. Should be one of the
* MigrateIdMapInterface::STATUS_* constants. Defaults to
* MigrateIdMapInterface::STATUS_FAILED.
*/
protected function mockFailure($migration, array $row, $status = MigrateIdMapInterface::STATUS_FAILED) {
if (is_string($migration)) {
$migration = $this->getMigration($migration);
}
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$destination = array_map(function () {
return NULL;
}, $migration->getDestinationPlugin()->getIds());
$row = new Row($row, $migration->getSourcePlugin()->getIds());
$migration->getIdMap()->saveIdMapping($row, $destination, $status);
}
/**
* Gets the migration plugin.
*
* @param $plugin_id
* The plugin ID of the migration to get.
*
* @return \Drupal\migrate\Plugin\Migration
* The migration plugin.
*/
protected function getMigration($plugin_id) {
return $this->container->get('plugin.manager.migration')->createInstance($plugin_id);
}
/**
* Injects the test logger into the container.
*/
protected function setTestLogger() {
$this->logger = $this->prophesize(LoggerChannelInterface::class);
$this->container->set('logger.channel.migrate', $this->logger->reveal());
\Drupal::setContainer($this->container);
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
/**
* Tests the migration plugin manager.
*
* @group migrate
*
* @coversDefaultClass \Drupal\migrate\Plugin\MigrationPluginManager
*/
class MigrationPluginManagerTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'migrate_tag_test'];
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManager
*/
protected $migrationPluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migrationPluginManager = \Drupal::service('plugin.manager.migration');
}
/**
* Tests Migration::createInstancesByTag().
*
* @covers ::createInstancesByTag
*
* @dataProvider providerCreateInstanceByTag
*/
public function testCreateInstancesByTag($tags, $expected): void {
// The test module includes a migration that does not use the migration_tags
// property. It is there to confirm that it is not included in the results.
// We create it to ensure it is a valid migration.
$migration = $this->migrationPluginManager->createInstances(['tag_test_no_tag']);
$this->assertArrayHasKey('tag_test_no_tag', $migration);
$migrations = $this->migrationPluginManager->createInstancesByTag($tags);
$actual = array_keys($migrations);
$this->assertSame($expected, $actual);
}
/**
* Data provider for testCreateInstancesByTag.
*/
public static function providerCreateInstanceByTag() {
return [
'get test' => [
'test',
['tag_test_0', 'tag_test_1'],
],
'get tag_test_1' => [
'tag_test_1',
['tag_test_1'],
],
'get no tags' => [
'',
[],
],
];
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the migration plugin.
*
* @group migrate
*
* @coversDefaultClass \Drupal\migrate\Plugin\Migration
*/
class MigrationTest extends KernelTestBase {
/**
* Enable field because we are using one of its source plugins.
*
* @var array
*/
protected static $modules = ['migrate', 'field'];
/**
* Tests Migration::set().
*
* @covers ::set
*/
public function testSetInvalidation(): void {
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration([
'source' => ['plugin' => 'empty'],
'destination' => ['plugin' => 'entity:entity_view_mode'],
]);
$this->assertEquals('empty', $migration->getSourcePlugin()->getPluginId());
$this->assertEquals('entity:entity_view_mode', $migration->getDestinationPlugin()->getPluginId());
// Test the source plugin is invalidated.
$migration->set('source', ['plugin' => 'embedded_data', 'data_rows' => [], 'ids' => []]);
$this->assertEquals('embedded_data', $migration->getSourcePlugin()->getPluginId());
// Test the destination plugin is invalidated.
$migration->set('destination', ['plugin' => 'null']);
$this->assertEquals('null', $migration->getDestinationPlugin()->getPluginId());
}
}

View File

@@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\Row;
use Drupal\user\Entity\User;
/**
* Tests the EntityExists process plugin.
*
* @group migrate
*/
class EntityExistsTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'system', 'user'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
}
/**
* Tests the EntityExists plugin.
*/
public function testEntityExists(): void {
$user = User::create([
'name' => $this->randomString(),
]);
$user->save();
$uid = $user->id();
$plugin = \Drupal::service('plugin.manager.migrate.process')
->createInstance('entity_exists', [
'entity_type' => 'user',
]);
$executable = $this->prophesize(MigrateExecutableInterface::class)->reveal();
$row = new Row();
// Ensure that the entity ID is returned if it really exists.
$value = $plugin->transform($uid, $executable, $row, 'buffalo');
$this->assertSame($uid, $value);
// Ensure that the plugin returns FALSE if the entity doesn't exist.
$value = $plugin->transform(420, $executable, $row, 'buffalo');
$this->assertFalse($value);
// Make sure the plugin can gracefully handle an array as input.
$value = $plugin->transform([$uid, 420], $executable, $row, 'buffalo');
$this->assertSame($uid, $value);
}
}

View File

@@ -0,0 +1,135 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\node\Entity\Node;
use Drupal\Tests\migrate\Kernel\MigrateTestBase;
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
// cspell:ignore tabarnak
/**
* Tests the EntityRevision destination plugin.
*
* @group migrate
*/
class EntityRevisionTest extends MigrateTestBase {
use ContentTypeCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'content_translation',
'field',
'filter',
'language',
'node',
'system',
'text',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('node');
$this->installEntitySchema('user');
$this->installConfig('node');
$this->installSchema('node', ['node_access']);
}
/**
* Tests that EntityRevision correctly handles revision translations.
*/
public function testRevisionTranslation(): void {
ConfigurableLanguage::createFromLangcode('fr')->save();
/** @var \Drupal\node\NodeInterface $node */
$node = Node::create([
'type' => $this->createContentType()->id(),
'title' => 'Default 1',
]);
$node->addTranslation('fr', [
'title' => 'French 1',
]);
$node->save();
$node->setNewRevision();
$node->setTitle('Default 2');
$node->getTranslation('fr')->setTitle('French 2');
$node->save();
$migration = [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
[
'nid' => $node->id(),
'vid' => $node->getRevisionId(),
'langcode' => 'fr',
'title' => 'Titre nouveau, tabarnak!',
],
],
'ids' => [
'nid' => [
'type' => 'integer',
],
'vid' => [
'type' => 'integer',
],
'langcode' => [
'type' => 'string',
],
],
],
'process' => [
'nid' => 'nid',
'vid' => 'vid',
'langcode' => 'langcode',
'title' => 'title',
],
'destination' => [
'plugin' => 'entity_revision:node',
'translations' => TRUE,
],
];
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = $this->container
->get('plugin.manager.migration')
->createStubMigration($migration);
$this->executeMigration($migration);
// The entity_revision destination uses the revision ID and langcode as its
// keys (the langcode is only used if the destination is configured for
// translation), so we should be able to look up the source IDs by revision
// ID and langcode.
$source_ids = $migration->getIdMap()->lookupSourceID([
'vid' => $node->getRevisionId(),
'langcode' => 'fr',
]);
$this->assertNotEmpty($source_ids);
$this->assertSame($node->id(), $source_ids['nid']);
$this->assertSame($node->getRevisionId(), $source_ids['vid']);
$this->assertSame('fr', $source_ids['langcode']);
// Confirm the french revision was used in the migration, instead of the
// default revision.
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = \Drupal::entityTypeManager();
$revision = $entity_type_manager->getStorage('node')->loadRevision(1);
$this->assertSame('Default 1', $revision->label());
$this->assertSame('French 1', $revision->getTranslation('fr')->label());
$revision = $entity_type_manager->getStorage('node')->loadRevision(2);
$this->assertSame('Default 2', $revision->label());
$this->assertSame('Titre nouveau, tabarnak!', $revision->getTranslation('fr')->label());
}
}

View File

@@ -0,0 +1,211 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin;
use Drupal\Core\Url;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateExecutable;
use Drupal\migrate\Row;
use Drupal\node\Entity\Node;
/**
* Tests the Log process plugin.
*
* @group migrate
*/
class LogTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['node', 'migrate'];
/**
* The Log process plugin.
*
* @var \Drupal\migrate\Plugin\migrate\process\Log
*/
protected $logPlugin;
/**
* Migrate executable.
*
* @var \Drupal\Tests\migrate\Kernel\Plugin\TestMigrateExecutable
*/
protected $executable;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$definition = [
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
['id' => '1'],
],
'ids' => [
'id' => ['type' => 'integer'],
],
],
'destination' => [
'plugin' => 'null',
],
];
/** @var \Drupal\migrate\Plugin\migration $migration */
$migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($definition);
$this->executable = new TestMigrateExecutable($migration);
// Plugin being tested.
$this->logPlugin = \Drupal::service('plugin.manager.migrate.process')
->createInstance('log');
}
/**
* Tests the Log plugin.
*/
public function testLog(): void {
$values = [
'nid' => 2,
'type' => 'page',
'title' => 'page',
];
$node = new Node($values, 'node', 'test');
$node_array = <<< NODE
Array
(
[nid] => Array
(
)
[uuid] => Array
(
)
[vid] => Array
(
)
[langcode] => Array
(
)
[type] => Array
(
)
[revision_timestamp] => Array
(
)
[revision_uid] => Array
(
)
[revision_log] => Array
(
)
[status] => Array
(
)
[uid] => Array
(
)
[title] => Array
(
)
[created] => Array
(
)
[changed] => Array
(
)
[promote] => Array
(
)
[sticky] => Array
(
)
[default_langcode] => Array
(
)
[revision_default] => Array
(
)
[revision_translation_affected] => Array
(
)
)
NODE;
$data = [
'node' => [
'value' => $node,
'expected_message' => "'foo' value is Drupal\\node\Entity\Node:\n'$node_array'",
],
'url' => [
'value' => Url::fromUri('https://en.wikipedia.org/wiki/Drupal#Community'),
'expected_message' => "'foo' value is Drupal\Core\Url:\n'https://en.wikipedia.org/wiki/Drupal#Community'",
],
];
$i = 1;
foreach ($data as $datum) {
$this->executable->sourceIdValues = ['id' => $i++];
// Test the input value is not altered.
$new_value = $this->logPlugin->transform($datum['value'], $this->executable, new Row(), 'foo');
$this->assertSame($datum['value'], $new_value);
// Test the stored message.
$message = $this->executable->getIdMap()
->getMessages($this->executable->sourceIdValues)
->fetchAllAssoc('message');
$actual_message = key($message);
$this->assertSame($datum['expected_message'], $actual_message);
}
}
}
/**
* MigrateExecutable test class.
*/
class TestMigrateExecutable extends MigrateExecutable {
/**
* The configuration values of the source.
*
* @var array
*/
public $sourceIdValues;
/**
* Get the ID map from the current migration.
*
* @return \Drupal\migrate\Plugin\MigrateIdMapInterface
* The ID map.
*/
public function getIdMap() {
return parent::getIdMap();
}
}

View File

@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin;
use Drupal\KernelTests\KernelTestBase;
/**
* Tests the migration plugin manager.
*
* @coversDefaultClass \Drupal\migrate\Plugin\MigratePluginManager
* @group migrate
*/
class MigrationPluginConfigurationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate',
'migrate_drupal',
// Test with a simple migration.
'ban',
'locale',
];
/**
* Tests merging configuration into a plugin through the plugin manager.
*
* @dataProvider mergeProvider
*/
public function testConfigurationMerge($id, $configuration, $expected): void {
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
$migration = $this->container->get('plugin.manager.migration')
->createInstance($id, $configuration);
$source_configuration = $migration->getSourceConfiguration();
$this->assertEquals($expected, $source_configuration);
}
/**
* Provide configuration data for testing.
*/
public static function mergeProvider() {
return [
// Tests adding new configuration to a migration.
[
// New configuration.
'd7_blocked_ips',
[
'source' => [
'constants' => [
'added_setting' => 'Ban them all!',
],
],
],
// Expected final source configuration.
[
'plugin' => 'd7_blocked_ips',
'constants' => [
'added_setting' => 'Ban them all!',
],
],
],
// Tests overriding pre-existing configuration in a migration.
[
// New configuration.
'd7_blocked_ips',
[
'source' => [
'plugin' => 'a_different_plugin',
],
],
// Expected final source configuration.
[
'plugin' => 'a_different_plugin',
],
],
// New configuration.
[
'locale_settings',
[
'source' => [
'plugin' => 'variable',
'variables' => [
'locale_cache_strings',
'locale_js_directory',
],
'source_module' => 'locale',
],
],
// Expected final source and process configuration.
[
'plugin' => 'variable',
'variables' => [
'locale_cache_strings',
'locale_js_directory',
],
'source_module' => 'locale',
],
],
];
}
}

View File

@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin;
use Drupal\Core\Database\Database;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
use Drupal\migrate\Plugin\RequirementsInterface;
use Drupal\Tests\field\Traits\EntityReferenceFieldCreationTrait;
/**
* Tests the migration plugin manager.
*
* @coversDefaultClass \Drupal\migrate\Plugin\MigratePluginManager
* @group migrate
*/
class MigrationPluginListTest extends KernelTestBase {
use EntityReferenceFieldCreationTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate',
// Test with all modules containing Drupal migrations.
'ban',
'block',
'block_content',
// @todo Remove book in https://www.drupal.org/project/drupal/issues/3376101
'book',
'comment',
'contact',
'content_translation',
'dblog',
'field',
'file',
'filter',
// @todo Remove forum in https://www.drupal.org/project/drupal/issues/3261653
'forum',
'image',
'language',
'locale',
'menu_link_content',
'menu_ui',
'node',
'options',
'path',
'search',
'shortcut',
// @todo Remove statistics in https://www.drupal.org/project/drupal/issues/3341092
'statistics',
'syslog',
'system',
'taxonomy',
'text',
// @todo Remove tracker in https://www.drupal.org/project/drupal/issues/3261452
'tracker',
'update',
'user',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->installEntitySchema('user');
}
/**
* @covers ::getDefinitions
*/
public function testGetDefinitions(): void {
// Create an entity reference field to make sure that migrations derived by
// EntityReferenceTranslationDeriver do not get discovered without
// migrate_drupal enabled.
$this->createEntityReferenceField('user', 'user', 'field_entity_reference', 'Entity Reference', 'node');
// Make sure retrieving all the core migration plugins does not throw any
// errors.
$migration_plugins = $this->container->get('plugin.manager.migration')->getDefinitions();
// All the plugins provided by core depend on migrate_drupal.
$this->assertEmpty($migration_plugins);
// Enable a module that provides migrations that do not depend on
// migrate_drupal.
$this->enableModules(['migrate_external_translated_test']);
$migration_plugins = $this->container->get('plugin.manager.migration')->getDefinitions();
// All the plugins provided by migrate_external_translated_test do not
// depend on migrate_drupal.
$this::assertArrayHasKey('external_translated_test_node', $migration_plugins);
$this::assertArrayHasKey('external_translated_test_node_translation', $migration_plugins);
// Disable the test module and the list should be empty again.
$this->disableModules(['migrate_external_translated_test']);
$migration_plugins = $this->container->get('plugin.manager.migration')->getDefinitions();
// All the plugins provided by core depend on migrate_drupal.
$this->assertEmpty($migration_plugins);
// Enable migrate_drupal to test that the plugins can now be discovered.
$this->enableModules(['migrate_drupal']);
// Make sure retrieving these migration plugins in the absence of a database
// connection does not throw any errors.
$migration_plugins = $this->container->get('plugin.manager.migration')->createInstances([]);
// Any database-based source plugins should fail a requirements test in the
// absence of a source database connection (e.g., a connection with the
// 'migrate' key).
$source_plugins = array_map(function ($migration_plugin) {
return $migration_plugin->getSourcePlugin();
}, $migration_plugins);
foreach ($source_plugins as $id => $source_plugin) {
if ($source_plugin instanceof RequirementsInterface) {
try {
$source_plugin->checkRequirements();
}
catch (RequirementsException $e) {
unset($source_plugins[$id]);
}
}
}
// Without a connection defined, no database-based plugins should be
// returned.
foreach ($source_plugins as $id => $source_plugin) {
$this->assertNotInstanceOf(SqlBase::class, $source_plugin);
}
// Set up a migrate database connection so that plugin discovery works.
// Clone the current connection and replace the current prefix.
$connection_info = Database::getConnectionInfo('migrate');
if ($connection_info) {
Database::renameConnection('migrate', 'simpletest_original_migrate');
}
$connection_info = Database::getConnectionInfo('default');
foreach ($connection_info as $target => $value) {
$prefix = $value['prefix'];
// Tests use 7 character prefixes at most so this can't cause collisions.
$connection_info[$target]['prefix'] = $prefix . '0';
}
Database::addConnectionInfo('migrate', 'default', $connection_info['default']);
// Make sure source plugins can be serialized.
foreach ($migration_plugins as $migration_plugin) {
$source_plugin = $migration_plugin->getSourcePlugin();
if ($source_plugin instanceof SqlBase) {
$source_plugin->getDatabase();
}
$this->assertNotEmpty(serialize($source_plugin));
}
$migration_plugins = $this->container->get('plugin.manager.migration')->getDefinitions();
// All the plugins provided by core depend on migrate_drupal.
$this->assertNotEmpty($migration_plugins);
// Test that migrations derived by EntityReferenceTranslationDeriver are
// discovered now that migrate_drupal is enabled.
$this->assertArrayHasKey('d6_entity_reference_translation:user__user', $migration_plugins);
$this->assertArrayHasKey('d7_entity_reference_translation:user__user', $migration_plugins);
}
}

View File

@@ -0,0 +1,246 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin;
use Drupal\KernelTests\FileSystemModuleDiscoveryDataProviderTrait;
use Drupal\migrate\Plugin\Exception\BadPluginDefinitionException;
use Drupal\migrate_drupal\Plugin\MigrateFieldPluginManager;
use Drupal\Tests\migrate_drupal\Kernel\MigrateDrupalTestBase;
// cspell:ignore entityreference filefield imagefield nodereference
// cspell:ignore optionwidgets userreference
/**
* Tests that modules exist for all source and destination plugins.
*
* @group migrate_drupal_ui
*/
class MigrationProvidersExistTest extends MigrateDrupalTestBase {
use FileSystemModuleDiscoveryDataProviderTrait;
/**
* Tests that a missing source_module property raises an exception.
*/
public function testSourceProvider(): void {
$this->enableModules(['migration_provider_test']);
$this->expectException(BadPluginDefinitionException::class);
$this->expectExceptionMessage('The no_source_module plugin must define the source_module property.');
$this->container->get('plugin.manager.migration')->getDefinition('migration_provider_no_annotation');
}
/**
* Tests that modules exist for all source plugins.
*/
public function testProvidersExist(): void {
$this->enableAllModules();
/** @var \Drupal\migrate\Plugin\MigrateSourcePluginManager $plugin_manager */
$plugin_manager = $this->container->get('plugin.manager.migrate.source');
foreach ($plugin_manager->getDefinitions() as $definition) {
$id = $definition['id'];
$this->assertArrayHasKey('source_module', $definition, "No source_module property in '$id'");
}
}
/**
* Enable all available modules.
*/
protected function enableAllModules() {
// Install all available modules.
$module_handler = $this->container->get('module_handler');
$modules = $this->coreModuleListDataProvider();
$modules_enabled = $module_handler->getModuleList();
$modules_to_enable = array_keys(array_diff_key($modules, $modules_enabled));
$this->enableModules($modules_to_enable);
}
/**
* Tests that modules exist for all field plugins.
*/
public function testFieldProvidersExist(): void {
$expected_mappings = [
'userreference' => [
'source_module' => 'userreference',
'destination_module' => 'core',
],
'nodereference' => [
'source_module' => 'nodereference',
'destination_module' => 'core',
],
'optionwidgets' => [
'source_module' => 'optionwidgets',
'destination_module' => 'options',
],
'list' => [
'source_module' => 'list',
'destination_module' => 'options',
],
'options' => [
'source_module' => 'options',
'destination_module' => 'options',
],
'filefield' => [
'source_module' => 'filefield',
'destination_module' => 'file',
],
'imagefield' => [
'source_module' => 'imagefield',
'destination_module' => 'image',
],
'file' => [
'source_module' => 'file',
'destination_module' => 'file',
],
'image' => [
'source_module' => 'image',
'destination_module' => 'image',
],
'phone' => [
'source_module' => 'phone',
'destination_module' => 'telephone',
],
'telephone' => [
'source_module' => 'telephone',
'destination_module' => 'telephone',
],
'link' => [
'source_module' => 'link',
'destination_module' => 'link',
],
'link_field' => [
'source_module' => 'link',
'destination_module' => 'link',
],
'd6_text' => [
'source_module' => 'text',
'destination_module' => 'text',
],
'd7_text' => [
'source_module' => 'text',
'destination_module' => 'text',
],
'taxonomy_term_reference' => [
'source_module' => 'taxonomy',
'destination_module' => 'core',
],
'date' => [
'source_module' => 'date',
'destination_module' => 'datetime',
],
'datetime' => [
'source_module' => 'date',
'destination_module' => 'datetime',
],
'email' => [
'source_module' => 'email',
'destination_module' => 'core',
],
'number_default' => [
'source_module' => 'number',
'destination_module' => 'core',
],
'entityreference' => [
'source_module' => 'entityreference',
'destination_module' => 'core',
],
'node_reference' => [
'source_module' => 'node_reference',
'destination_module' => 'core',
],
'user_reference' => [
'source_module' => 'user_reference',
'destination_module' => 'core',
],
];
$this->enableAllModules();
$definitions = $this->container->get('plugin.manager.migrate.field')->getDefinitions();
foreach ($definitions as $key => $definition) {
$this->assertArrayHasKey($key, $expected_mappings);
$this->assertEquals($expected_mappings[$key]['source_module'], $definition['source_module']);
$this->assertEquals($expected_mappings[$key]['destination_module'], $definition['destination_module']);
}
}
/**
* Tests a missing required definition.
*
* @param array $definitions
* A field plugin definition.
* @param string $missing_property
* The name of the property missing from the definition.
*
* @dataProvider fieldPluginDefinitionsProvider
*/
public function testFieldProviderMissingRequiredProperty(array $definitions, $missing_property): void {
$discovery = $this->getMockBuilder(MigrateFieldPluginManager::class)
->disableOriginalConstructor()
->onlyMethods(['getDefinitions'])
->getMock();
$discovery->method('getDefinitions')
->willReturn($definitions);
$plugin_manager = $this->getMockBuilder(MigrateFieldPluginManager::class)
->disableOriginalConstructor()
->onlyMethods(['getDiscovery'])
->getMock();
$plugin_manager->method('getDiscovery')
->willReturn($discovery);
$this->expectException(BadPluginDefinitionException::class);
$this->expectExceptionMessage("The missing_{$missing_property} plugin must define the $missing_property property.");
$plugin_manager->getDefinitions();
}
/**
* Data provider for field plugin definitions.
*
* @return array
* Array of plugin definitions.
*/
public static function fieldPluginDefinitionsProvider() {
return [
'missing_core_scenario' => [
'definitions' => [
'missing_core' => [
'source_module' => 'migrate',
'destination_module' => 'migrate',
'id' => 'missing_core',
'class' => 'foo',
'provider' => 'foo',
],
],
'missing_property' => 'core',
],
'missing_source_scenario' => [
'definitions' => [
'missing_source_module' => [
'core' => [6, 7],
'destination_module' => 'migrate',
'id' => 'missing_source_module',
'class' => 'foo',
'provider' => 'foo',
],
],
'missing_property' => 'source_module',
],
'missing_destination_scenario' => [
'definitions' => [
'missing_destination_module' => [
'core' => [6, 7],
'source_module' => 'migrate',
'id' => 'missing_destination_module',
'class' => 'foo',
'provider' => 'foo',
],
],
'missing_property' => 'destination_module',
],
];
}
}

View File

@@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\MigrateSkipRowException;
/**
* Tests the migration plugin.
*
* @coversDefaultClass \Drupal\migrate\Plugin\Migration
* @group migrate
*/
class MigrationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate', 'migrate_expected_migrations_test'];
/**
* Tests Migration::getProcessPlugins()
*
* @covers ::getProcessPlugins
*/
public function testGetProcessPlugins(): void {
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration([]);
$this->assertEquals([], $migration->getProcessPlugins([]));
}
/**
* Tests Migration::getProcessPlugins() throws an exception.
*
* @covers ::getProcessPlugins
*/
public function testGetProcessPluginsException(): void {
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration([]);
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('Invalid process configuration for foobar');
$migration->getProcessPlugins(['foobar' => ['plugin' => 'get']]);
}
/**
* Tests Migration::getProcessPlugins()
*
* @param array $process
* The migration process pipeline.
*
* @covers ::getProcessPlugins
*
* @dataProvider getProcessPluginsExceptionMessageProvider
*/
public function testGetProcessPluginsExceptionMessage(array $process): void {
// Test with an invalid process pipeline.
$plugin_definition = [
'id' => 'foo',
'process' => $process,
];
$destination = array_key_first(($process));
$migration = \Drupal::service('plugin.manager.migration')
->createStubMigration($plugin_definition);
$this->expectException(MigrateException::class);
$this->expectExceptionMessage("Invalid process for destination '$destination' in migration 'foo'");
$migration->getProcessPlugins();
}
/**
* Provides data for testing invalid process pipeline.
*/
public static function getProcessPluginsExceptionMessageProvider(): \Generator {
yield 'null' => ['process' => ['dest' => NULL]];
yield 'boolean' => ['process' => ['dest' => TRUE]];
yield 'integer' => ['process' => ['dest' => 2370]];
yield 'float' => ['process' => ['dest' => 1.61]];
}
/**
* Tests Migration::getMigrationDependencies()
*
* @covers ::getMigrationDependencies
*/
public function testGetMigrationDependencies(): void {
$plugin_manager = \Drupal::service('plugin.manager.migration');
$plugin_definition = [
'id' => 'foo',
'deriver' => 'fooDeriver',
'process' => [
'f1' => 'bar',
'f2' => [
'plugin' => 'migration',
'migration' => 'm1',
],
'f3' => [
'plugin' => 'sub_process',
'process' => [
'target_id' => [
'plugin' => 'migration',
'migration' => 'm2',
],
],
],
'f4' => [
'plugin' => 'migration_lookup',
'migration' => 'm3',
],
'f5' => [
'plugin' => 'sub_process',
'process' => [
'target_id' => [
'plugin' => 'migration_lookup',
'migration' => 'm4',
],
],
],
'f6' => [
'plugin' => 'iterator',
'process' => [
'target_id' => [
'plugin' => 'migration_lookup',
'migration' => 'm5',
],
],
],
'f7' => [
'plugin' => 'migration_lookup',
'migration' => 'foo',
],
],
];
$migration = $plugin_manager->createStubMigration($plugin_definition);
$this->assertSame(['required' => [], 'optional' => ['m1', 'm2', 'm3', 'm4', 'm5']], $migration->getMigrationDependencies(TRUE));
}
/**
* Tests Migration::getDestinationIds()
*
* @covers ::getDestinationIds
*/
public function testGetDestinationIds(): void {
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration(['destinationIds' => ['foo' => 'bar']]);
$destination_ids = $migration->getDestinationIds();
$this->assertNotEmpty($destination_ids, 'Destination ids are not empty');
$this->assertEquals(['foo' => 'bar'], $destination_ids, 'Destination ids match the expected values.');
}
/**
* Tests Migration::getTrackLastImported()
*
* @covers ::getTrackLastImported
* @covers ::isTrackLastImported
*
* @group legacy
*/
public function testGetTrackLastImported(): void {
$this->expectDeprecation('Drupal\migrate\Plugin\Migration::setTrackLastImported() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3282894');
$this->expectDeprecation('Drupal\migrate\Plugin\Migration::getTrackLastImported() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3282894');
$this->expectDeprecation('Drupal\migrate\Plugin\Migration::isTrackLastImported() is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. There is no replacement. See https://www.drupal.org/node/3282894');
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration([]);
$migration->setTrackLastImported(TRUE);
$this->assertEquals(TRUE, $migration->getTrackLastImported());
$this->assertEquals(TRUE, $migration->isTrackLastImported());
}
/**
* Tests Migration::getDestinationPlugin()
*
* @covers ::getDestinationPlugin
*/
public function testGetDestinationPlugin(): void {
$migration = \Drupal::service('plugin.manager.migration')->createStubMigration(['destination' => ['no_stub' => TRUE]]);
$this->expectException(MigrateSkipRowException::class);
$this->expectExceptionMessage("Stub requested but not made because no_stub configuration is set.");
$migration->getDestinationPlugin(TRUE);
}
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin\id_map;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\Plugin\migrate\id_map\Sql;
/**
* Tests deprecation notice in Sql constructor.
*
* @group migrate
* @group legacy
*/
class SqlDeprecationTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate'];
/**
* @covers \Drupal\migrate\Plugin\migrate\id_map\Sql::__construct
*/
public function testOptionalParametersDeprecation(): void {
$migration = $this->prophesize('\Drupal\migrate\Plugin\MigrationInterface')->reveal();
$this->expectDeprecation('Calling Sql::__construct() without the $migration_manager argument is deprecated in drupal:9.5.0 and the $migration_manager argument will be required in drupal:11.0.0. See https://www.drupal.org/node/3277306');
new Sql(
[],
'sql',
[],
$migration,
$this->container->get('event_dispatcher')
);
}
}

View File

@@ -0,0 +1,206 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin\id_map;
use Drupal\Core\Database\Database;
use Drupal\Core\Database\Exception\SchemaTableColumnSizeTooLargeException;
use Drupal\Tests\migrate\Kernel\MigrateTestBase;
use Drupal\Tests\migrate\Unit\TestSqlIdMap;
use Drupal\migrate\MigrateException;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Tests that the migrate map table is created.
*
* @group migrate
*/
class SqlTest extends MigrateTestBase {
/**
* Database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $database;
/**
* Prophesized event dispatcher.
*
* @var object|\Prophecy\Prophecy\ProphecySubjectInterface|\Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Definition of a test migration.
*
* @var array
*/
protected $migrationDefinition;
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManager
*/
protected $migrationPluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->database = \Drupal::database();
$this->eventDispatcher = $this->prophesize(EventDispatcherInterface::class)
->reveal();
$this->migrationPluginManager = \Drupal::service('plugin.manager.migration');
$this->migrationDefinition = [
'id' => 'test',
'source' => [
'plugin' => 'embedded_data',
'data_rows' => [
[
'alpha' => '1',
'bravo' => '2',
'charlie' => '3',
'delta' => '4',
'echo' => '5',
],
],
'ids' => [],
],
'process' => [],
'destination' => [
'plugin' => 'null',
],
];
}
/**
* Tests that ensureTables creates the migrate map table.
*
* @dataProvider providerTestEnsureTables
*/
public function testEnsureTables($ids): void {
$this->migrationDefinition['source']['ids'] = $ids;
$migration = $this->migrationPluginManager->createStubMigration($this->migrationDefinition);
$map = new TestSqlIdMap($this->database, [], 'test', [], $migration, $this->eventDispatcher, $this->migrationPluginManager);
$map->ensureTables();
// Checks that the map table was created.
$exists = $this->database->schema()->tableExists('migrate_map_test');
$this->assertTrue($exists);
}
/**
* Provides data for testEnsureTables.
*/
public static function providerTestEnsureTables() {
return [
'no ids' => [
[],
],
'one id' => [
[
'alpha' => [
'type' => 'string',
],
],
],
'too many' => [
[
'alpha' => [
'type' => 'string',
],
'bravo' => [
'type' => 'string',
],
'charlie' => [
'type' => 'string',
],
'delta' => [
'type' => 'string',
],
'echo ' => [
'type' => 'string',
],
],
],
];
}
/**
* Tests exception is thrown in ensureTables fails.
*
* @dataProvider providerTestFailEnsureTables
*/
public function testFailEnsureTables($ids): void {
// This just tests mysql, as other PDO integrations allow longer indexes.
if (Database::getConnection()->databaseType() !== 'mysql') {
$this->markTestSkipped("This test only runs for MySQL");
}
$this->migrationDefinition['source']['ids'] = $ids;
$migration = $this->container
->get('plugin.manager.migration')
->createStubMigration($this->migrationDefinition);
// Use local id map plugin to force an error.
$map = new SqlIdMapTest($this->database, [], 'test', [], $migration, $this->eventDispatcher, $this->migrationPluginManager);
$this->expectException(SchemaTableColumnSizeTooLargeException::class);
$map->ensureTables();
}
/**
* Provides data for testFailEnsureTables.
*/
public static function providerTestFailEnsureTables() {
return [
'one id' => [
[
'alpha' => [
'type' => 'string',
],
],
],
];
}
}
/**
* Defines a test SQL ID map for use in tests.
*/
class SqlIdMapTest extends TestSqlIdMap implements \Iterator {
/**
* {@inheritdoc}
*/
protected function getFieldSchema(array $id_definition) {
if (!isset($id_definition['type'])) {
return [];
}
switch ($id_definition['type']) {
case 'integer':
return [
'type' => 'int',
'not null' => TRUE,
];
case 'string':
return [
'type' => 'varchar',
'length' => 65536,
'not null' => FALSE,
];
default:
throw new MigrateException($id_definition['type'] . ' not supported');
}
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin\source;
use Drupal\Tests\migrate\Kernel\MigrateSqlSourceTestBase;
/**
* Tests SqlBase source count caching.
*
* @covers \Drupal\migrate_sql_count_cache_test\Plugin\migrate\source\SqlCountCache
* @covers \Drupal\migrate\Plugin\migrate\source\SqlBase::doCount
* @covers \Drupal\migrate\Plugin\migrate\source\SourcePluginBase::count
*
* @group migrate
*/
class MigrateSqlSourceCountCacheTest extends MigrateSqlSourceTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_sql_count_cache_test'];
/**
* {@inheritdoc}
*/
public static function providerSource() {
// All tests use the same source_data, expected_data, expected_count, and
// high_water. The high water is set later to maintain the order of the
// parameters.
$data = [
'source_data' => [
'source_table' => [
['id' => 1],
['id' => 2],
['id' => 3],
['id' => 4],
],
],
'expected_data' => [
['id' => 1],
['id' => 2],
['id' => 3],
['id' => 4],
],
'expected_count' => 4,
];
return [
'uncached source count' => $data,
'cached source count, auto-generated cache key' => $data + [
'configuration' => [
'cache_counts' => TRUE,
],
'high_water' => NULL,
'expected_cache_key' => 'sql_count_cache-dbed2396c230e025663091479993a206441bf1f9ae4e60ebf3b504e4a76ad471',
],
'cached source count, auto-generated cache key for alternative source configuration' => $data + [
'configuration' => [
'cache_counts' => TRUE,
'some_source_plugin_configuration_key' => 19920106,
],
'high_water' => NULL,
'expected_cache_key' => 'sql_count_cache-83c62856dd5afc011f32574bcdc11c595557d629e1d73045e9353df2441ec269',
],
'cached source count, provided cache key' => $data + [
'configuration' => [
'cache_counts' => TRUE,
'cache_key' => 'custom_cache_key_here',
],
'high_water' => NULL,
'expected_cache_key' => 'custom_cache_key_here',
],
];
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel\Plugin\source;
use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
use Drupal\Tests\migrate\Kernel\MigrateTestBase;
/**
* Test source counts are correctly cached.
*
* @group migrate
*/
class MigrationSourceCacheTest extends MigrateTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['migrate_cache_counts_test'];
/**
* The migration plugin manager.
*
* @var \Drupal\migrate\Plugin\MigrationPluginManagerInterface
*/
protected $migrationPluginManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migrationPluginManager = $this->container->get('plugin.manager.migration');
}
/**
* Tests that counts for the same plugin_id are not crossed.
*/
public function testCacheCountsNotContaminated(): void {
$migration_1_definition = [
'source' => [
'plugin' => 'cacheable_embedded_data',
'cache_counts' => TRUE,
'ids' => [
'id' => [
'type' => 'integer',
],
],
'data_rows' => [
[
['id' => 1],
],
],
],
];
$migration_2_definition = [
'source' => [
'plugin' => 'cacheable_embedded_data',
'cache_counts' => TRUE,
'ids' => [
'id' => [
'type' => 'integer',
],
],
'data_rows' => [
['id' => 1],
['id' => 2],
],
],
];
$migration_1 = $this->migrationPluginManager->createStubMigration($migration_1_definition);
$migration_2 = $this->migrationPluginManager->createStubMigration($migration_2_definition);
$migration_1_source = $migration_1->getSourcePlugin();
$migration_2_source = $migration_2->getSourcePlugin();
// Verify correct counts when count is refreshed.
$this->assertSame(1, $migration_1_source->count(TRUE));
$this->assertSame(2, $migration_2_source->count(TRUE));
// Verify correct counts are cached.
$this->assertCount(1, $migration_1_source);
$this->assertCount(2, $migration_2_source);
// Verify the cache keys are different.
$cache_key_property = new \ReflectionProperty(SourcePluginBase::class, 'cacheKey');
$this->assertNotEquals($cache_key_property->getValue($migration_1_source), $cache_key_property->getValue($migration_2_source));
}
/**
* Test that values are pulled from the cache when appropriate.
*/
public function testCacheCountsUsed(): void {
$migration_definition = [
'source' => [
'plugin' => 'cacheable_embedded_data',
'cache_counts' => TRUE,
'ids' => [
'id' => [
'type' => 'integer',
],
],
'data_rows' => [
['id' => 1],
['id' => 2],
],
],
];
$migration = $this->migrationPluginManager->createStubMigration($migration_definition);
$migration_source = $migration->getSourcePlugin();
$this->assertCount(2, $migration_source);
// Pollute the cache.
$cache_key_property = new \ReflectionProperty($migration_source, 'cacheKey');
$cache_key = $cache_key_property->getValue($migration_source);
\Drupal::cache('migrate')->set($cache_key, 7);
$this->assertCount(7, $migration_source);
$this->assertSame(2, $migration_source->count(TRUE));
}
}

View File

@@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\KernelTests\KernelTestBase;
use Drupal\migrate\MigrateException;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\sqlite\Driver\Database\sqlite\Connection;
use Drupal\TestTools\Random;
/**
* Tests query batching.
*
* @covers \Drupal\migrate_query_batch_test\Plugin\migrate\source\QueryBatchTest
* @group migrate
*/
class QueryBatchTest extends KernelTestBase {
/**
* The mocked migration.
*
* @var \Drupal\migrate\Plugin\MigrationInterface|\Prophecy\Prophecy\ObjectProphecy
*/
protected $migration;
/**
* {@inheritdoc}
*/
protected static $modules = [
'migrate',
'migrate_query_batch_test',
];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
// Create a mock migration. This will be injected into the source plugin
// under test.
$this->migration = $this->prophesize(MigrationInterface::class);
$this->migration->id()->willReturn(
$this->randomMachineName(16)
);
// Prophesize a useless ID map plugin and an empty set of destination IDs.
// Calling code can override these prophecies later and set up different
// behaviors.
$this->migration->getIdMap()->willReturn(
$this->prophesize(MigrateIdMapInterface::class)->reveal()
);
$this->migration->getDestinationIds()->willReturn([]);
}
/**
* Tests a negative batch size throws an exception.
*/
public function testBatchSizeNegative(): void {
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('batch_size must be greater than or equal to zero');
$plugin = $this->getPlugin(['batch_size' => -1]);
$plugin->next();
}
/**
* Tests a non integer batch size throws an exception.
*/
public function testBatchSizeNonInteger(): void {
$this->expectException(MigrateException::class);
$this->expectExceptionMessage('batch_size must be greater than or equal to zero');
$plugin = $this->getPlugin(['batch_size' => '1']);
$plugin->next();
}
/**
* {@inheritdoc}
*/
public static function queryDataProvider() {
// Define the parameters for building the data array. The first element is
// the number of source data rows, the second is the batch size to set on
// the plugin configuration.
$test_parameters = [
// Test when batch size is 0.
[200, 0],
// Test when rows mod batch size is 0.
[200, 20],
// Test when rows mod batch size is > 0.
[200, 30],
// Test when batch size = row count.
[200, 200],
// Test when batch size > row count.
[200, 300],
];
// Build the data provider array. The provider array consists of the source
// data rows, the expected result data, the expected count, the plugin
// configuration, the expected batch size and the expected batch count.
$table = 'query_batch_test';
$tests = [];
$data_set = 0;
foreach ($test_parameters as $data) {
[$num_rows, $batch_size] = $data;
for ($i = 0; $i < $num_rows; $i++) {
$tests[$data_set]['source_data'][$table][] = [
'id' => $i,
'data' => Random::string(),
];
}
$tests[$data_set]['expected_data'] = $tests[$data_set]['source_data'][$table];
$tests[$data_set][2] = $num_rows;
// Plugin configuration array.
$tests[$data_set][3] = ['batch_size' => $batch_size];
// Expected batch size.
$tests[$data_set][4] = $batch_size;
// Expected batch count is 0 unless a batch size is set.
$expected_batch_count = 0;
if ($batch_size > 0) {
$expected_batch_count = (int) ($num_rows / $batch_size);
if ($num_rows % $batch_size) {
// If there is a remainder an extra batch is needed to get the
// remaining rows.
$expected_batch_count++;
}
}
$tests[$data_set][5] = $expected_batch_count;
$data_set++;
}
return $tests;
}
/**
* Tests query batch size.
*
* @param array $source_data
* The source data, keyed by table name. Each table is an array containing
* the rows in that table.
* @param array $expected_data
* The result rows the plugin is expected to return.
* @param int $num_rows
* How many rows the source plugin is expected to return.
* @param array $configuration
* Configuration for the source plugin specifying the batch size.
* @param int $expected_batch_size
* The expected batch size, will be set to zero for invalid batch sizes.
* @param int $expected_batch_count
* The total number of batches.
*
* @dataProvider queryDataProvider
*/
public function testQueryBatch($source_data, $expected_data, $num_rows, $configuration, $expected_batch_size, $expected_batch_count): void {
$plugin = $this->getPlugin($configuration);
// Since we don't yet inject the database connection, we need to use a
// reflection hack to set it in the plugin instance.
$reflector = new \ReflectionObject($plugin);
$property = $reflector->getProperty('database');
$connection = $this->getDatabase($source_data);
$property->setValue($plugin, $connection);
// Test the results.
$i = 0;
/** @var \Drupal\migrate\Row $row */
foreach ($plugin as $row) {
$expected = $expected_data[$i++];
$actual = $row->getSource();
foreach ($expected as $key => $value) {
$this->assertArrayHasKey($key, $actual);
$this->assertSame((string) $value, (string) $actual[$key]);
}
}
// Test that all rows were retrieved.
self::assertSame($num_rows, $i);
// Test the batch size.
if (is_null($expected_batch_size)) {
$expected_batch_size = $configuration['batch_size'];
}
$property = $reflector->getProperty('batchSize');
self::assertSame($expected_batch_size, $property->getValue($plugin));
// Test the batch count.
if (is_null($expected_batch_count)) {
$expected_batch_count = intdiv($num_rows, $expected_batch_size);
if ($num_rows % $configuration['batch_size']) {
$expected_batch_count++;
}
}
$property = $reflector->getProperty('batch');
self::assertSame($expected_batch_count, $property->getValue($plugin));
}
/**
* Instantiates the source plugin under test.
*
* @param array $configuration
* The source plugin configuration.
*
* @return \Drupal\migrate\Plugin\MigrateSourceInterface|object
* The fully configured source plugin.
*/
protected function getPlugin($configuration) {
/** @var \Drupal\migrate\Plugin\MigratePluginManager $plugin_manager */
$plugin_manager = $this->container->get('plugin.manager.migrate.source');
$plugin = $plugin_manager->createInstance('query_batch_test', $configuration, $this->migration->reveal());
$this->migration
->getSourcePlugin()
->willReturn($plugin);
return $plugin;
}
/**
* Builds an in-memory SQLite database from a set of source data.
*
* @param array $source_data
* The source data, keyed by table name. Each table is an array containing
* the rows in that table.
*
* @return \Drupal\sqlite\Driver\Database\sqlite\Connection
* The SQLite database connection.
*/
protected function getDatabase(array $source_data) {
// Create an in-memory SQLite database. Plugins can interact with it like
// any other database, and it will cease to exist when the connection is
// closed.
$connection_options = ['database' => ':memory:'];
$pdo = Connection::open($connection_options);
$connection = new Connection($pdo, $connection_options);
// Create the tables and fill them with data.
foreach ($source_data as $table => $rows) {
// Use the biggest row to build the table schema.
$counts = array_map('count', $rows);
asort($counts);
$pilot = $rows[array_key_last($counts)];
$connection->schema()
->createTable($table, [
// SQLite uses loose affinity typing, so it's OK for every field to
// be a text field.
'fields' => array_map(function () {
return ['type' => 'text'];
}, $pilot),
]);
$fields = array_keys($pilot);
$insert = $connection->insert($table)->fields($fields);
array_walk($rows, [$insert, 'values']);
$insert->execute();
}
return $connection;
}
}

View File

@@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\Core\Database\Query\ConditionInterface;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Database\StatementInterface;
use Drupal\migrate\Exception\RequirementsException;
use Drupal\Core\Database\Database;
use Drupal\migrate\Plugin\migrate\source\SqlBase;
use Drupal\migrate\Plugin\MigrationInterface;
/**
* Tests the functionality of SqlBase.
*
* @group migrate
*/
class SqlBaseTest extends MigrateTestBase {
/**
* The (probably mocked) migration under test.
*
* @var \Drupal\migrate\Plugin\MigrationInterface
*/
protected $migration;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->migration = $this->createMock(MigrationInterface::class);
$this->migration->method('id')->willReturn('foo');
}
/**
* Tests different connection types.
*/
public function testConnectionTypes(): void {
$sql_base = new TestSqlBase([], $this->migration);
// Verify that falling back to the default 'migrate' connection (defined in
// the base class) works.
$this->assertSame('default', $sql_base->getDatabase()->getTarget());
$this->assertSame('migrate', $sql_base->getDatabase()->getKey());
// Verify the fallback state key overrides the 'migrate' connection.
$target = 'test_fallback_target';
$key = 'test_fallback_key';
$config = ['target' => $target, 'key' => $key];
$database_state_key = 'test_fallback_state';
\Drupal::state()->set($database_state_key, $config);
\Drupal::state()->set('migrate.fallback_state_key', $database_state_key);
// Create a test connection using the default database configuration.
Database::addConnectionInfo($key, $target, Database::getConnectionInfo('default')['default']);
$this->assertSame($sql_base->getDatabase()->getTarget(), $target);
$this->assertSame($sql_base->getDatabase()->getKey(), $key);
// Verify that setting explicit connection information overrides fallbacks.
$target = 'test_db_target';
$key = 'test_migrate_connection';
$config = ['target' => $target, 'key' => $key];
$sql_base->setConfiguration($config);
Database::addConnectionInfo($key, $target, Database::getConnectionInfo('default')['default']);
// Validate we have injected our custom key and target.
$this->assertSame($sql_base->getDatabase()->getTarget(), $target);
$this->assertSame($sql_base->getDatabase()->getKey(), $key);
// Now test we can have SqlBase create the connection from an info array.
$sql_base = new TestSqlBase([], $this->migration);
$target = 'test_db_target2';
$key = 'test_migrate_connection2';
$database = Database::getConnectionInfo('default')['default'];
$config = ['target' => $target, 'key' => $key, 'database' => $database];
$sql_base->setConfiguration($config);
// Call getDatabase() to get the connection defined.
$sql_base->getDatabase();
// Validate the connection has been created with the right values.
$this->assertSame(Database::getConnectionInfo($key)[$target], $database);
// Now, test this all works when using state to store db info.
$target = 'test_state_db_target';
$key = 'test_state_migrate_connection';
$config = ['target' => $target, 'key' => $key];
$database_state_key = 'migrate_sql_base_test';
\Drupal::state()->set($database_state_key, $config);
$sql_base->setConfiguration(['database_state_key' => $database_state_key]);
Database::addConnectionInfo($key, $target, Database::getConnectionInfo('default')['default']);
// Validate we have injected our custom key and target.
$this->assertSame($sql_base->getDatabase()->getTarget(), $target);
$this->assertSame($sql_base->getDatabase()->getKey(), $key);
// Now test we can have SqlBase create the connection from an info array.
$sql_base = new TestSqlBase([], $this->migration);
$target = 'test_state_db_target2';
$key = 'test_state_migrate_connection2';
$database = Database::getConnectionInfo('default')['default'];
$config = ['target' => $target, 'key' => $key, 'database' => $database];
$database_state_key = 'migrate_sql_base_test2';
\Drupal::state()->set($database_state_key, $config);
$sql_base->setConfiguration(['database_state_key' => $database_state_key]);
// Call getDatabase() to get the connection defined.
$sql_base->getDatabase();
// Validate the connection has been created with the right values.
$this->assertSame(Database::getConnectionInfo($key)[$target], $database);
// Verify that falling back to 'migrate' when the connection is not defined
// throws a RequirementsException.
\Drupal::state()->delete('migrate.fallback_state_key');
$sql_base->setConfiguration([]);
Database::renameConnection('migrate', 'fallback_connection');
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage('No database connection configured for source plugin');
$sql_base->getDatabase();
}
/**
* Tests the exception when a connection is defined but not available.
*/
public function testBrokenConnection(): void {
if (Database::getConnection()->driver() === 'sqlite') {
$this->markTestSkipped('Not compatible with sqlite');
}
$sql_base = new TestSqlBase([], $this->migration);
$target = 'test_state_db_target2';
$key = 'test_state_migrate_connection2';
$database = Database::getConnectionInfo('default')['default'];
$database['database'] = 'godot';
$config = ['target' => $target, 'key' => $key, 'database' => $database];
$database_state_key = 'migrate_sql_base_test2';
\Drupal::state()->set($database_state_key, $config);
$sql_base->setConfiguration(['database_state_key' => $database_state_key]);
// Call checkRequirements(): it will call getDatabase() and convert the
// exception to a RequirementsException.
$this->expectException(RequirementsException::class);
$this->expectExceptionMessage('No database connection available for source plugin sql_base');
$sql_base->checkRequirements();
}
/**
* Tests that SqlBase respects high-water values.
*
* @param mixed $high_water
* (optional) The high-water value to set.
* @param array $query_result
* (optional) The expected query results.
*
* @dataProvider highWaterDataProvider
*/
public function testHighWater($high_water = NULL, array $query_result = []): void {
$configuration = [
'high_water_property' => [
'name' => 'order',
],
];
$source = new TestSqlBase($configuration, $this->migration);
if ($high_water) {
\Drupal::keyValue('migrate:high_water')->set($this->migration->id(), $high_water);
}
$statement = $this->createMock(StatementInterface::class);
$statement->expects($this->atLeastOnce())->method('setFetchMode')->with(\PDO::FETCH_ASSOC);
$query = $this->createMock(SelectInterface::class);
$query->method('execute')->willReturn($statement);
$query->expects($this->atLeastOnce())->method('orderBy')->with('order', 'ASC');
$condition_group = $this->createMock(ConditionInterface::class);
$query->method('orConditionGroup')->willReturn($condition_group);
$source->setQuery($query);
$source->rewind();
}
/**
* Data provider for ::testHighWater().
*
* @return array
* The scenarios to test.
*/
public static function highWaterDataProvider() {
return [
'no high-water value set' => [],
'high-water value set' => [33],
];
}
}
/**
* A dummy source to help with testing SqlBase.
*
* @package Drupal\migrate\Plugin\migrate\source
*/
class TestSqlBase extends SqlBase {
/**
* The query to execute.
*
* @var \Drupal\Core\Database\Query\SelectInterface
*/
protected $query;
/**
* Overrides the constructor so we can create one easily.
*
* @param array $configuration
* The plugin instance configuration.
* @param \Drupal\migrate\Plugin\MigrationInterface $migration
* (optional) The migration being run.
*/
public function __construct(array $configuration = [], ?MigrationInterface $migration = NULL) {
parent::__construct($configuration, 'sql_base', ['requirements_met' => TRUE], $migration, \Drupal::state());
}
/**
* Gets the database without caching it.
*/
public function getDatabase() {
$this->database = NULL;
return parent::getDatabase();
}
/**
* Allows us to set the configuration from a test.
*
* @param array $config
* The config array.
*/
public function setConfiguration($config) {
$this->configuration = $config;
}
/**
* {@inheritdoc}
*/
public function getIds() {
return [];
}
/**
* {@inheritdoc}
*/
public function fields() {
throw new \RuntimeException(__METHOD__ . " not implemented for " . __CLASS__);
}
/**
* {@inheritdoc}
*/
public function query() {
return $this->query;
}
/**
* Sets the query to execute.
*
* @param \Drupal\Core\Database\Query\SelectInterface $query
* The query to execute.
*/
public function setQuery(SelectInterface $query) {
$this->query = $query;
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
/**
* Class to test FilterIterators.
*/
class TestFilterIterator extends \FilterIterator {
/**
* {@inheritdoc}
*/
public function accept(): bool {
return TRUE;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Drupal\Tests\migrate\Kernel;
use Drupal\migrate\MigrateExecutable;
/**
* Tests MigrateExecutable.
*/
class TestMigrateExecutable extends MigrateExecutable {
/**
* {@inheritdoc}
*/
protected function getIdMap() {
// This adds test coverage that this works.
return new TestFilterIterator(parent::getIdMap());
}
/**
* {@inheritdoc}
*/
protected function getSource() {
// This adds test coverage that this works.
return new TestFilterIterator(parent::getSource());
}
}

Some files were not shown because too many files have changed in this diff Show More