'test_migration', 'source' => [], ]; /** * Test row data. * * @var array */ protected $row = ['test_sourceid1' => '1', 'timestamp' => 500]; /** * Test source ids. * * @var array */ protected $sourceIds = ['test_sourceid1' => 'test_sourceid1']; /** * The migration entity. * * @var \Drupal\migrate\Plugin\MigrationInterface */ protected $migration; /** * The migrate executable. * * @var \Drupal\migrate\MigrateExecutable */ protected $executable; /** * Gets the source plugin to test. * * @param array $configuration * (optional) The source configuration. Defaults to an empty array. * @param array $migrate_config * (optional) The migration configuration to be used in * parent::getMigration(). Defaults to an empty array. * @param int $status * (optional) The default status for the new rows to be imported. Defaults * to MigrateIdMapInterface::STATUS_NEEDS_UPDATE. * * @return \Drupal\migrate\Plugin\MigrateSourceInterface * A mocked source plugin. */ protected function getSource($configuration = [], $migrate_config = [], $status = MigrateIdMapInterface::STATUS_NEEDS_UPDATE, $high_water_value = NULL) { $container = new ContainerBuilder(); \Drupal::setContainer($container); $key_value = $this->getMock(KeyValueStoreInterface::class); $key_value_factory = $this->getMock(KeyValueFactoryInterface::class); $key_value_factory ->method('get') ->with('migrate:high_water') ->willReturn($key_value); $container->set('keyvalue', $key_value_factory); $container->set('cache.migrate', $this->getMock(CacheBackendInterface::class)); $this->migrationConfiguration = $this->defaultMigrationConfiguration + $migrate_config; $this->migration = parent::getMigration(); $this->executable = $this->getMigrateExecutable($this->migration); // Update the idMap for Source so the default is that the row has already // been imported. This allows us to use the highwater mark to decide on the // outcome of whether we choose to import the row. $id_map_array = ['original_hash' => '', 'hash' => '', 'source_row_status' => $status]; $this->idMap ->expects($this->any()) ->method('getRowBySource') ->willReturn($id_map_array); $constructor_args = [$configuration, 'd6_action', [], $this->migration]; $methods = ['getModuleHandler', 'fields', 'getIds', '__toString', 'prepareRow', 'initializeIterator']; $source_plugin = $this->getMock(SourcePluginBase::class, $methods, $constructor_args); $source_plugin ->method('fields') ->willReturn([]); $source_plugin ->method('getIds') ->willReturn([]); $source_plugin ->method('__toString') ->willReturn(''); $source_plugin ->method('prepareRow') ->willReturn(empty($migrate_config['prepare_row_false'])); $rows = [$this->row]; if (isset($configuration['high_water_property']) && isset($high_water_value)) { $property = $configuration['high_water_property']['name']; $rows = array_filter($rows, function (array $row) use ($property, $high_water_value) { return $row[$property] >= $high_water_value; }); } $iterator = new \ArrayIterator($rows); $source_plugin ->method('initializeIterator') ->willReturn($iterator); $module_handler = $this->getMock(ModuleHandlerInterface::class); $source_plugin ->method('getModuleHandler') ->willReturn($module_handler); $this->migration ->method('getSourcePlugin') ->willReturn($source_plugin); return $source_plugin; } /** * @covers ::__construct */ public function testHighwaterTrackChangesIncompatible() { $source_config = ['track_changes' => TRUE, 'high_water_property' => ['name' => 'something']]; $this->setExpectedException(MigrateException::class); $this->getSource($source_config); } /** * Test that the source count is correct. * * @covers ::count */ public function testCount() { // Mock the cache to validate set() receives appropriate arguments. $container = new ContainerBuilder(); $cache = $this->getMock(CacheBackendInterface::class); $cache->expects($this->any())->method('set') ->with($this->isType('string'), $this->isType('int'), $this->isType('int')); $container->set('cache.migrate', $cache); \Drupal::setContainer($container); // Test that the basic count works. $source = $this->getSource(); $this->assertEquals(1, $source->count()); // Test caching the count works. $source = $this->getSource(['cache_counts' => TRUE]); $this->assertEquals(1, $source->count()); // Test the skip argument. $source = $this->getSource(['skip_count' => TRUE]); $this->assertEquals(-1, $source->count()); $this->migrationConfiguration['id'] = 'test_migration'; $migration = $this->getMigration(); $source = new StubSourceGeneratorPlugin([], '', [], $migration); // Test the skipCount property's default value. $this->assertEquals(-1, $source->count()); // Test the count value using a generator. $source = new StubSourceGeneratorPlugin(['skip_count' => FALSE], '', [], $migration); $this->assertEquals(3, $source->count()); } /** * Test that the key can be set for the count cache. * * @covers ::count */ public function testCountCacheKey() { // Mock the cache to validate set() receives appropriate arguments. $container = new ContainerBuilder(); $cache = $this->getMock(CacheBackendInterface::class); $cache->expects($this->any())->method('set') ->with('test_key', $this->isType('int'), $this->isType('int')); $container->set('cache.migrate', $cache); \Drupal::setContainer($container); // Test caching the count with a configured key works. $source = $this->getSource(['cache_counts' => TRUE, 'cache_key' => 'test_key']); $this->assertEquals(1, $source->count()); } /** * Test that we don't get a row if prepareRow() is false. */ public function testPrepareRowFalse() { $source = $this->getSource([], ['prepare_row_false' => TRUE]); $source->rewind(); $this->assertNull($source->current(), 'No row is available when prepareRow() is false.'); } /** * Test that $row->needsUpdate() works as expected. */ public function testNextNeedsUpdate() { $source = $this->getSource(); // $row->needsUpdate() === TRUE so we get a row. $source->rewind(); $this->assertTrue(is_a($source->current(), 'Drupal\migrate\Row'), '$row->needsUpdate() is TRUE so we got a row.'); // Test that we don't get a row when the incoming row is marked as imported. $source = $this->getSource([], [], MigrateIdMapInterface::STATUS_IMPORTED); $source->rewind(); $this->assertNull($source->current(), 'Row was already imported, should be NULL'); } /** * Test that an outdated highwater mark does not cause a row to be imported. */ public function testOutdatedHighwater() { $configuration = [ 'high_water_property' => [ 'name' => 'timestamp', ], ]; $source = $this->getSource($configuration, [], MigrateIdMapInterface::STATUS_IMPORTED, $this->row['timestamp'] + 1); // The current highwater mark is now higher than the row timestamp so no row // is expected. $source->rewind(); $this->assertNull($source->current(), 'Original highwater mark is higher than incoming row timestamp.'); } /** * Test that a highwater mark newer than our saved one imports a row. * * @throws \Exception */ public function testNewHighwater() { $configuration = [ 'high_water_property' => [ 'name' => 'timestamp', ], ]; // Set a highwater property field for source. Now we should have a row // because the row timestamp is greater than the current highwater mark. $source = $this->getSource($configuration, [], MigrateIdMapInterface::STATUS_IMPORTED, $this->row['timestamp'] - 1); $source->rewind(); $this->assertInstanceOf(Row::class, $source->current(), 'Incoming row timestamp is greater than current highwater mark so we have a row.'); } /** * Test basic row preparation. * * @covers ::prepareRow */ public function testPrepareRow() { $this->migrationConfiguration['id'] = 'test_migration'; // Get a new migration with an id. $migration = $this->getMigration(); $source = new StubSourcePlugin([], '', [], $migration); $row = new Row(); $module_handler = $this->prophesize(ModuleHandlerInterface::class); $module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration]) ->willReturn([TRUE, TRUE]) ->shouldBeCalled(); $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration]) ->willReturn([TRUE, TRUE]) ->shouldBeCalled(); $source->setModuleHandler($module_handler->reveal()); // Ensure we don't log this to the mapping table. $this->idMap->expects($this->never()) ->method('saveIdMapping'); $this->assertTrue($source->prepareRow($row)); // Track_changes... $source = new StubSourcePlugin(['track_changes' => TRUE], '', [], $migration); $row2 = $this->prophesize(Row::class); $row2->rehash() ->shouldBeCalled(); $module_handler->invokeAll('migrate_prepare_row', [$row2, $source, $migration]) ->willReturn([TRUE, TRUE]) ->shouldBeCalled(); $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row2, $source, $migration]) ->willReturn([TRUE, TRUE]) ->shouldBeCalled(); $source->setModuleHandler($module_handler->reveal()); $this->assertTrue($source->prepareRow($row2->reveal())); } /** * Test that global prepare hooks can skip rows. * * @covers ::prepareRow */ public function testPrepareRowGlobalPrepareSkip() { $this->migrationConfiguration['id'] = 'test_migration'; $migration = $this->getMigration(); $source = new StubSourcePlugin([], '', [], $migration); $row = new Row(); $module_handler = $this->prophesize(ModuleHandlerInterface::class); // Return a failure from a prepare row hook. $module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration]) ->willReturn([TRUE, FALSE, TRUE]) ->shouldBeCalled(); $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration]) ->willReturn([TRUE, TRUE]) ->shouldBeCalled(); $source->setModuleHandler($module_handler->reveal()); $this->idMap->expects($this->once()) ->method('saveIdMapping') ->with($row, [], MigrateIdMapInterface::STATUS_IGNORED); $this->assertFalse($source->prepareRow($row)); } /** * Test that migrate specific prepare hooks can skip rows. * * @covers ::prepareRow */ public function testPrepareRowMigratePrepareSkip() { $this->migrationConfiguration['id'] = 'test_migration'; $migration = $this->getMigration(); $source = new StubSourcePlugin([], '', [], $migration); $row = new Row(); $module_handler = $this->prophesize(ModuleHandlerInterface::class); // Return a failure from a prepare row hook. $module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration]) ->willReturn([TRUE, TRUE]) ->shouldBeCalled(); $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration]) ->willReturn([TRUE, FALSE, TRUE]) ->shouldBeCalled(); $source->setModuleHandler($module_handler->reveal()); $this->idMap->expects($this->once()) ->method('saveIdMapping') ->with($row, [], MigrateIdMapInterface::STATUS_IGNORED); $this->assertFalse($source->prepareRow($row)); } /** * Test that a skip exception during prepare hooks correctly skips. * * @covers ::prepareRow */ public function testPrepareRowPrepareException() { $this->migrationConfiguration['id'] = 'test_migration'; $migration = $this->getMigration(); $source = new StubSourcePlugin([], '', [], $migration); $row = new Row(); $module_handler = $this->prophesize(ModuleHandlerInterface::class); // Return a failure from a prepare row hook. $module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration]) ->willReturn([TRUE, TRUE]) ->shouldBeCalled(); $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration]) ->willThrow(new MigrateSkipRowException()) ->shouldBeCalled(); $source->setModuleHandler($module_handler->reveal()); // This will only be called on the first prepare because the second // explicitly avoids it. $this->idMap->expects($this->once()) ->method('saveIdMapping') ->with($row, [], MigrateIdMapInterface::STATUS_IGNORED); $this->assertFalse($source->prepareRow($row)); // Throw an exception the second time that avoids mapping. $e = new MigrateSkipRowException('', FALSE); $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration]) ->willThrow($e) ->shouldBeCalled(); $this->assertFalse($source->prepareRow($row)); } /** * Test that cacheCounts, skipCount, trackChanges preserve their default * values. */ public function testDefaultPropertiesValues() { $this->migrationConfiguration['id'] = 'test_migration'; $migration = $this->getMigration(); $source = new StubSourceGeneratorPlugin([], '', [], $migration); // Test the default value of the skipCount Value; $this->assertTrue($source->getSkipCount()); $this->assertTrue($source->getCacheCounts()); $this->assertTrue($source->getTrackChanges()); } /** * Gets a mock executable for the test. * * @param \Drupal\migrate\Plugin\MigrationInterface $migration * The migration entity. * * @return \Drupal\migrate\MigrateExecutable * The migrate executable. */ protected function getMigrateExecutable($migration) { /** @var \Drupal\migrate\MigrateMessageInterface $message */ $message = $this->getMock('Drupal\migrate\MigrateMessageInterface'); /** @var \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher */ $event_dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface'); return new MigrateExecutable($migration, $message, $event_dispatcher); } } /** * Stubbed source plugin for testing base class implementations. */ class StubSourcePlugin extends SourcePluginBase { /** * Helper for setting internal module handler implementation. * * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. */ public function setModuleHandler(ModuleHandlerInterface $module_handler) { $this->moduleHandler = $module_handler; } /** * {@inheritdoc} */ public function fields() { return []; } /** * {@inheritdoc} */ public function __toString() { return ''; } /** * {@inheritdoc} */ public function getIds() { return []; } /** * {@inheritdoc} */ protected function initializeIterator() { return []; } } /** * Stubbed source plugin with a generator as iterator. Also it overwrites the * $skipCount, $cacheCounts and $trackChanges properties. */ class StubSourceGeneratorPlugin extends StubSourcePlugin { /** * {@inheritdoc} */ protected $skipCount = TRUE; /** * {@inheritdoc} */ protected $cacheCounts = TRUE; /** * {@inheritdoc} */ protected $trackChanges = TRUE; /** * Return the skipCount value. */ public function getSkipCount() { return $this->skipCount; } /** * Return the cacheCounts value. */ public function getCacheCounts() { return $this->cacheCounts; } /** * Return the trackChanges value. */ public function getTrackChanges() { return $this->trackChanges; } /** * {@inheritdoc} */ protected function initializeIterator() { $data = [ ['title' => 'foo'], ['title' => 'bar'], ['title' => 'iggy'], ]; foreach ($data as $row) { yield $row; } } }