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