5 * Contains \Drupal\Tests\migrate\Unit\MigrateSourceTest.
8 namespace Drupal\Tests\migrate\Unit;
10 use Drupal\Core\Cache\CacheBackendInterface;
11 use Drupal\Core\DependencyInjection\ContainerBuilder;
12 use Drupal\Core\Extension\ModuleHandlerInterface;
13 use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
14 use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
15 use Drupal\migrate\MigrateException;
16 use Drupal\migrate\MigrateExecutable;
17 use Drupal\migrate\MigrateSkipRowException;
18 use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
19 use Drupal\migrate\Plugin\MigrateIdMapInterface;
20 use Drupal\migrate\Row;
23 * @coversDefaultClass \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
26 class MigrateSourceTest extends MigrateTestCase {
29 * Override the migration config.
33 protected $defaultMigrationConfiguration = [
34 'id' => 'test_migration',
43 protected $row = ['test_sourceid1' => '1', 'timestamp' => 500];
50 protected $sourceIds = ['test_sourceid1' => 'test_sourceid1'];
53 * The migration entity.
55 * @var \Drupal\migrate\Plugin\MigrationInterface
60 * The migrate executable.
62 * @var \Drupal\migrate\MigrateExecutable
64 protected $executable;
67 * Gets the source plugin to test.
69 * @param array $configuration
70 * (optional) The source configuration. Defaults to an empty array.
71 * @param array $migrate_config
72 * (optional) The migration configuration to be used in
73 * parent::getMigration(). Defaults to an empty array.
75 * (optional) The default status for the new rows to be imported. Defaults
76 * to MigrateIdMapInterface::STATUS_NEEDS_UPDATE.
78 * @return \Drupal\migrate\Plugin\MigrateSourceInterface
79 * A mocked source plugin.
81 protected function getSource($configuration = [], $migrate_config = [], $status = MigrateIdMapInterface::STATUS_NEEDS_UPDATE, $high_water_value = NULL) {
82 $container = new ContainerBuilder();
83 \Drupal::setContainer($container);
85 $key_value = $this->getMock(KeyValueStoreInterface::class);
87 $key_value_factory = $this->getMock(KeyValueFactoryInterface::class);
90 ->with('migrate:high_water')
91 ->willReturn($key_value);
92 $container->set('keyvalue', $key_value_factory);
94 $container->set('cache.migrate', $this->getMock(CacheBackendInterface::class));
96 $this->migrationConfiguration = $this->defaultMigrationConfiguration + $migrate_config;
97 $this->migration = parent::getMigration();
98 $this->executable = $this->getMigrateExecutable($this->migration);
100 // Update the idMap for Source so the default is that the row has already
101 // been imported. This allows us to use the highwater mark to decide on the
102 // outcome of whether we choose to import the row.
103 $id_map_array = ['original_hash' => '', 'hash' => '', 'source_row_status' => $status];
105 ->expects($this->any())
106 ->method('getRowBySource')
107 ->willReturn($id_map_array);
109 $constructor_args = [$configuration, 'd6_action', [], $this->migration];
110 $methods = ['getModuleHandler', 'fields', 'getIds', '__toString', 'prepareRow', 'initializeIterator'];
111 $source_plugin = $this->getMock(SourcePluginBase::class, $methods, $constructor_args);
120 ->method('__toString')
123 ->method('prepareRow')
124 ->willReturn(empty($migrate_config['prepare_row_false']));
126 $rows = [$this->row];
127 if (isset($configuration['high_water_property']) && isset($high_water_value)) {
128 $property = $configuration['high_water_property']['name'];
129 $rows = array_filter($rows, function (array $row) use ($property, $high_water_value) {
130 return $row[$property] >= $high_water_value;
133 $iterator = new \ArrayIterator($rows);
136 ->method('initializeIterator')
137 ->willReturn($iterator);
139 $module_handler = $this->getMock(ModuleHandlerInterface::class);
141 ->method('getModuleHandler')
142 ->willReturn($module_handler);
145 ->method('getSourcePlugin')
146 ->willReturn($source_plugin);
148 return $source_plugin;
152 * @covers ::__construct
154 public function testHighwaterTrackChangesIncompatible() {
155 $source_config = ['track_changes' => TRUE, 'high_water_property' => ['name' => 'something']];
156 $this->setExpectedException(MigrateException::class);
157 $this->getSource($source_config);
161 * Test that the source count is correct.
165 public function testCount() {
166 // Mock the cache to validate set() receives appropriate arguments.
167 $container = new ContainerBuilder();
168 $cache = $this->getMock(CacheBackendInterface::class);
169 $cache->expects($this->any())->method('set')
170 ->with($this->isType('string'), $this->isType('int'), $this->isType('int'));
171 $container->set('cache.migrate', $cache);
172 \Drupal::setContainer($container);
174 // Test that the basic count works.
175 $source = $this->getSource();
176 $this->assertEquals(1, $source->count());
178 // Test caching the count works.
179 $source = $this->getSource(['cache_counts' => TRUE]);
180 $this->assertEquals(1, $source->count());
182 // Test the skip argument.
183 $source = $this->getSource(['skip_count' => TRUE]);
184 $this->assertEquals(-1, $source->count());
186 $this->migrationConfiguration['id'] = 'test_migration';
187 $migration = $this->getMigration();
188 $source = new StubSourceGeneratorPlugin([], '', [], $migration);
190 // Test the skipCount property's default value.
191 $this->assertEquals(-1, $source->count());
193 // Test the count value using a generator.
194 $source = new StubSourceGeneratorPlugin(['skip_count' => FALSE], '', [], $migration);
195 $this->assertEquals(3, $source->count());
199 * Test that the key can be set for the count cache.
203 public function testCountCacheKey() {
204 // Mock the cache to validate set() receives appropriate arguments.
205 $container = new ContainerBuilder();
206 $cache = $this->getMock(CacheBackendInterface::class);
207 $cache->expects($this->any())->method('set')
208 ->with('test_key', $this->isType('int'), $this->isType('int'));
209 $container->set('cache.migrate', $cache);
210 \Drupal::setContainer($container);
212 // Test caching the count with a configured key works.
213 $source = $this->getSource(['cache_counts' => TRUE, 'cache_key' => 'test_key']);
214 $this->assertEquals(1, $source->count());
218 * Test that we don't get a row if prepareRow() is false.
220 public function testPrepareRowFalse() {
221 $source = $this->getSource([], ['prepare_row_false' => TRUE]);
224 $this->assertNull($source->current(), 'No row is available when prepareRow() is false.');
228 * Test that $row->needsUpdate() works as expected.
230 public function testNextNeedsUpdate() {
231 $source = $this->getSource();
233 // $row->needsUpdate() === TRUE so we get a row.
235 $this->assertTrue(is_a($source->current(), 'Drupal\migrate\Row'), '$row->needsUpdate() is TRUE so we got a row.');
237 // Test that we don't get a row when the incoming row is marked as imported.
238 $source = $this->getSource([], [], MigrateIdMapInterface::STATUS_IMPORTED);
240 $this->assertNull($source->current(), 'Row was already imported, should be NULL');
244 * Test that an outdated highwater mark does not cause a row to be imported.
246 public function testOutdatedHighwater() {
248 'high_water_property' => [
249 'name' => 'timestamp',
252 $source = $this->getSource($configuration, [], MigrateIdMapInterface::STATUS_IMPORTED, $this->row['timestamp'] + 1);
254 // The current highwater mark is now higher than the row timestamp so no row
257 $this->assertNull($source->current(), 'Original highwater mark is higher than incoming row timestamp.');
261 * Test that a highwater mark newer than our saved one imports a row.
265 public function testNewHighwater() {
267 'high_water_property' => [
268 'name' => 'timestamp',
271 // Set a highwater property field for source. Now we should have a row
272 // because the row timestamp is greater than the current highwater mark.
273 $source = $this->getSource($configuration, [], MigrateIdMapInterface::STATUS_IMPORTED, $this->row['timestamp'] - 1);
276 $this->assertInstanceOf(Row::class, $source->current(), 'Incoming row timestamp is greater than current highwater mark so we have a row.');
280 * Test basic row preparation.
282 * @covers ::prepareRow
284 public function testPrepareRow() {
285 $this->migrationConfiguration['id'] = 'test_migration';
287 // Get a new migration with an id.
288 $migration = $this->getMigration();
289 $source = new StubSourcePlugin([], '', [], $migration);
292 $module_handler = $this->prophesize(ModuleHandlerInterface::class);
293 $module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration])
294 ->willReturn([TRUE, TRUE])
296 $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
297 ->willReturn([TRUE, TRUE])
299 $source->setModuleHandler($module_handler->reveal());
301 // Ensure we don't log this to the mapping table.
302 $this->idMap->expects($this->never())
303 ->method('saveIdMapping');
305 $this->assertTrue($source->prepareRow($row));
308 $source = new StubSourcePlugin(['track_changes' => TRUE], '', [], $migration);
309 $row2 = $this->prophesize(Row::class);
312 $module_handler->invokeAll('migrate_prepare_row', [$row2, $source, $migration])
313 ->willReturn([TRUE, TRUE])
315 $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row2, $source, $migration])
316 ->willReturn([TRUE, TRUE])
318 $source->setModuleHandler($module_handler->reveal());
319 $this->assertTrue($source->prepareRow($row2->reveal()));
323 * Test that global prepare hooks can skip rows.
325 * @covers ::prepareRow
327 public function testPrepareRowGlobalPrepareSkip() {
328 $this->migrationConfiguration['id'] = 'test_migration';
330 $migration = $this->getMigration();
331 $source = new StubSourcePlugin([], '', [], $migration);
334 $module_handler = $this->prophesize(ModuleHandlerInterface::class);
335 // Return a failure from a prepare row hook.
336 $module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration])
337 ->willReturn([TRUE, FALSE, TRUE])
339 $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
340 ->willReturn([TRUE, TRUE])
342 $source->setModuleHandler($module_handler->reveal());
344 $this->idMap->expects($this->once())
345 ->method('saveIdMapping')
346 ->with($row, [], MigrateIdMapInterface::STATUS_IGNORED);
348 $this->assertFalse($source->prepareRow($row));
352 * Test that migrate specific prepare hooks can skip rows.
354 * @covers ::prepareRow
356 public function testPrepareRowMigratePrepareSkip() {
357 $this->migrationConfiguration['id'] = 'test_migration';
359 $migration = $this->getMigration();
360 $source = new StubSourcePlugin([], '', [], $migration);
363 $module_handler = $this->prophesize(ModuleHandlerInterface::class);
364 // Return a failure from a prepare row hook.
365 $module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration])
366 ->willReturn([TRUE, TRUE])
368 $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
369 ->willReturn([TRUE, FALSE, TRUE])
371 $source->setModuleHandler($module_handler->reveal());
373 $this->idMap->expects($this->once())
374 ->method('saveIdMapping')
375 ->with($row, [], MigrateIdMapInterface::STATUS_IGNORED);
377 $this->assertFalse($source->prepareRow($row));
381 * Test that a skip exception during prepare hooks correctly skips.
383 * @covers ::prepareRow
385 public function testPrepareRowPrepareException() {
386 $this->migrationConfiguration['id'] = 'test_migration';
388 $migration = $this->getMigration();
389 $source = new StubSourcePlugin([], '', [], $migration);
392 $module_handler = $this->prophesize(ModuleHandlerInterface::class);
393 // Return a failure from a prepare row hook.
394 $module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration])
395 ->willReturn([TRUE, TRUE])
397 $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
398 ->willThrow(new MigrateSkipRowException())
400 $source->setModuleHandler($module_handler->reveal());
402 // This will only be called on the first prepare because the second
403 // explicitly avoids it.
404 $this->idMap->expects($this->once())
405 ->method('saveIdMapping')
406 ->with($row, [], MigrateIdMapInterface::STATUS_IGNORED);
407 $this->assertFalse($source->prepareRow($row));
409 // Throw an exception the second time that avoids mapping.
410 $e = new MigrateSkipRowException('', FALSE);
411 $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
414 $this->assertFalse($source->prepareRow($row));
418 * Test that cacheCounts, skipCount, trackChanges preserve their default
421 public function testDefaultPropertiesValues() {
422 $this->migrationConfiguration['id'] = 'test_migration';
423 $migration = $this->getMigration();
424 $source = new StubSourceGeneratorPlugin([], '', [], $migration);
426 // Test the default value of the skipCount Value;
427 $this->assertTrue($source->getSkipCount());
428 $this->assertTrue($source->getCacheCounts());
429 $this->assertTrue($source->getTrackChanges());
433 * Gets a mock executable for the test.
435 * @param \Drupal\migrate\Plugin\MigrationInterface $migration
436 * The migration entity.
438 * @return \Drupal\migrate\MigrateExecutable
439 * The migrate executable.
441 protected function getMigrateExecutable($migration) {
442 /** @var \Drupal\migrate\MigrateMessageInterface $message */
443 $message = $this->getMock('Drupal\migrate\MigrateMessageInterface');
444 /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher */
445 $event_dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
446 return new MigrateExecutable($migration, $message, $event_dispatcher);
452 * Stubbed source plugin for testing base class implementations.
454 class StubSourcePlugin extends SourcePluginBase {
457 * Helper for setting internal module handler implementation.
459 * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
460 * The module handler.
462 public function setModuleHandler(ModuleHandlerInterface $module_handler) {
463 $this->moduleHandler = $module_handler;
469 public function fields() {
476 public function __toString() {
483 public function getIds() {
490 protected function initializeIterator() {
497 * Stubbed source plugin with a generator as iterator. Also it overwrites the
498 * $skipCount, $cacheCounts and $trackChanges properties.
500 class StubSourceGeneratorPlugin extends StubSourcePlugin {
505 protected $skipCount = TRUE;
510 protected $cacheCounts = TRUE;
515 protected $trackChanges = TRUE;
518 * Return the skipCount value.
520 public function getSkipCount() {
521 return $this->skipCount;
525 * Return the cacheCounts value.
527 public function getCacheCounts() {
528 return $this->cacheCounts;
532 * Return the trackChanges value.
534 public function getTrackChanges() {
535 return $this->trackChanges;
541 protected function initializeIterator() {
547 foreach ($data as $row) {