Version 1
[yaffs-website] / web / core / modules / migrate / tests / src / Unit / MigrateSourceTest.php
diff --git a/web/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php b/web/core/modules/migrate/tests/src/Unit/MigrateSourceTest.php
new file mode 100644 (file)
index 0000000..7f3d293
--- /dev/null
@@ -0,0 +1,552 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Tests\migrate\Unit\MigrateSourceTest.
+ */
+
+namespace Drupal\Tests\migrate\Unit;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\DependencyInjection\ContainerBuilder;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+use Drupal\migrate\MigrateException;
+use Drupal\migrate\MigrateExecutable;
+use Drupal\migrate\MigrateSkipRowException;
+use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
+use Drupal\migrate\Plugin\MigrateIdMapInterface;
+use Drupal\migrate\Row;
+
+/**
+ * @coversDefaultClass \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
+ * @group migrate
+ */
+class MigrateSourceTest extends MigrateTestCase {
+
+  /**
+   * Override the migration config.
+   *
+   * @var array
+   */
+  protected $defaultMigrationConfiguration = [
+    'id' => '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;
+    }
+  }
+
+}