Version 1
[yaffs-website] / web / core / modules / migrate / tests / src / Unit / MigrateSourceTest.php
1 <?php
2
3 /**
4  * @file
5  * Contains \Drupal\Tests\migrate\Unit\MigrateSourceTest.
6  */
7
8 namespace Drupal\Tests\migrate\Unit;
9
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;
21
22 /**
23  * @coversDefaultClass \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
24  * @group migrate
25  */
26 class MigrateSourceTest extends MigrateTestCase {
27
28   /**
29    * Override the migration config.
30    *
31    * @var array
32    */
33   protected $defaultMigrationConfiguration = [
34     'id' => 'test_migration',
35     'source' => [],
36   ];
37
38   /**
39    * Test row data.
40    *
41    * @var array
42    */
43   protected $row = ['test_sourceid1' => '1', 'timestamp' => 500];
44
45   /**
46    * Test source ids.
47    *
48    * @var array
49    */
50   protected $sourceIds = ['test_sourceid1' => 'test_sourceid1'];
51
52   /**
53    * The migration entity.
54    *
55    * @var \Drupal\migrate\Plugin\MigrationInterface
56    */
57   protected $migration;
58
59   /**
60    * The migrate executable.
61    *
62    * @var \Drupal\migrate\MigrateExecutable
63    */
64   protected $executable;
65
66   /**
67    * Gets the source plugin to test.
68    *
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.
74    * @param int $status
75    *   (optional) The default status for the new rows to be imported. Defaults
76    *   to MigrateIdMapInterface::STATUS_NEEDS_UPDATE.
77    *
78    * @return \Drupal\migrate\Plugin\MigrateSourceInterface
79    *   A mocked source plugin.
80    */
81   protected function getSource($configuration = [], $migrate_config = [], $status = MigrateIdMapInterface::STATUS_NEEDS_UPDATE, $high_water_value = NULL) {
82     $container = new ContainerBuilder();
83     \Drupal::setContainer($container);
84
85     $key_value = $this->getMock(KeyValueStoreInterface::class);
86
87     $key_value_factory = $this->getMock(KeyValueFactoryInterface::class);
88     $key_value_factory
89       ->method('get')
90       ->with('migrate:high_water')
91       ->willReturn($key_value);
92     $container->set('keyvalue', $key_value_factory);
93
94     $container->set('cache.migrate', $this->getMock(CacheBackendInterface::class));
95
96     $this->migrationConfiguration = $this->defaultMigrationConfiguration + $migrate_config;
97     $this->migration = parent::getMigration();
98     $this->executable = $this->getMigrateExecutable($this->migration);
99
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];
104     $this->idMap
105       ->expects($this->any())
106       ->method('getRowBySource')
107       ->willReturn($id_map_array);
108
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);
112
113     $source_plugin
114       ->method('fields')
115       ->willReturn([]);
116     $source_plugin
117       ->method('getIds')
118       ->willReturn([]);
119     $source_plugin
120       ->method('__toString')
121       ->willReturn('');
122     $source_plugin
123       ->method('prepareRow')
124       ->willReturn(empty($migrate_config['prepare_row_false']));
125
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;
131       });
132     }
133     $iterator = new \ArrayIterator($rows);
134
135     $source_plugin
136       ->method('initializeIterator')
137       ->willReturn($iterator);
138
139     $module_handler = $this->getMock(ModuleHandlerInterface::class);
140     $source_plugin
141       ->method('getModuleHandler')
142       ->willReturn($module_handler);
143
144     $this->migration
145       ->method('getSourcePlugin')
146       ->willReturn($source_plugin);
147
148     return $source_plugin;
149   }
150
151   /**
152    * @covers ::__construct
153    */
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);
158   }
159
160   /**
161    * Test that the source count is correct.
162    *
163    * @covers ::count
164    */
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);
173
174     // Test that the basic count works.
175     $source = $this->getSource();
176     $this->assertEquals(1, $source->count());
177
178     // Test caching the count works.
179     $source = $this->getSource(['cache_counts' => TRUE]);
180     $this->assertEquals(1, $source->count());
181
182     // Test the skip argument.
183     $source = $this->getSource(['skip_count' => TRUE]);
184     $this->assertEquals(-1, $source->count());
185
186     $this->migrationConfiguration['id'] = 'test_migration';
187     $migration = $this->getMigration();
188     $source = new StubSourceGeneratorPlugin([], '', [], $migration);
189
190     // Test the skipCount property's default value.
191     $this->assertEquals(-1, $source->count());
192
193     // Test the count value using a generator.
194     $source = new StubSourceGeneratorPlugin(['skip_count' => FALSE], '', [], $migration);
195     $this->assertEquals(3, $source->count());
196   }
197
198   /**
199    * Test that the key can be set for the count cache.
200    *
201    * @covers ::count
202    */
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);
211
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());
215   }
216
217   /**
218    * Test that we don't get a row if prepareRow() is false.
219    */
220   public function testPrepareRowFalse() {
221     $source = $this->getSource([], ['prepare_row_false' => TRUE]);
222
223     $source->rewind();
224     $this->assertNull($source->current(), 'No row is available when prepareRow() is false.');
225   }
226
227   /**
228    * Test that $row->needsUpdate() works as expected.
229    */
230   public function testNextNeedsUpdate() {
231     $source = $this->getSource();
232
233     // $row->needsUpdate() === TRUE so we get a row.
234     $source->rewind();
235     $this->assertTrue(is_a($source->current(), 'Drupal\migrate\Row'), '$row->needsUpdate() is TRUE so we got a row.');
236
237     // Test that we don't get a row when the incoming row is marked as imported.
238     $source = $this->getSource([], [], MigrateIdMapInterface::STATUS_IMPORTED);
239     $source->rewind();
240     $this->assertNull($source->current(), 'Row was already imported, should be NULL');
241   }
242
243   /**
244    * Test that an outdated highwater mark does not cause a row to be imported.
245    */
246   public function testOutdatedHighwater() {
247     $configuration = [
248       'high_water_property' => [
249         'name' => 'timestamp',
250       ],
251     ];
252     $source = $this->getSource($configuration, [], MigrateIdMapInterface::STATUS_IMPORTED, $this->row['timestamp'] + 1);
253
254     // The current highwater mark is now higher than the row timestamp so no row
255     // is expected.
256     $source->rewind();
257     $this->assertNull($source->current(), 'Original highwater mark is higher than incoming row timestamp.');
258   }
259
260   /**
261    * Test that a highwater mark newer than our saved one imports a row.
262    *
263    * @throws \Exception
264    */
265   public function testNewHighwater() {
266     $configuration = [
267       'high_water_property' => [
268         'name' => 'timestamp',
269       ],
270     ];
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);
274
275     $source->rewind();
276     $this->assertInstanceOf(Row::class, $source->current(), 'Incoming row timestamp is greater than current highwater mark so we have a row.');
277   }
278
279   /**
280    * Test basic row preparation.
281    *
282    * @covers ::prepareRow
283    */
284   public function testPrepareRow() {
285     $this->migrationConfiguration['id'] = 'test_migration';
286
287     // Get a new migration with an id.
288     $migration = $this->getMigration();
289     $source = new StubSourcePlugin([], '', [], $migration);
290     $row = new Row();
291
292     $module_handler = $this->prophesize(ModuleHandlerInterface::class);
293     $module_handler->invokeAll('migrate_prepare_row', [$row, $source, $migration])
294       ->willReturn([TRUE, TRUE])
295       ->shouldBeCalled();
296     $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
297       ->willReturn([TRUE, TRUE])
298       ->shouldBeCalled();
299     $source->setModuleHandler($module_handler->reveal());
300
301     // Ensure we don't log this to the mapping table.
302     $this->idMap->expects($this->never())
303       ->method('saveIdMapping');
304
305     $this->assertTrue($source->prepareRow($row));
306
307     // Track_changes...
308     $source = new StubSourcePlugin(['track_changes' => TRUE], '', [], $migration);
309     $row2 = $this->prophesize(Row::class);
310     $row2->rehash()
311       ->shouldBeCalled();
312     $module_handler->invokeAll('migrate_prepare_row', [$row2, $source, $migration])
313       ->willReturn([TRUE, TRUE])
314       ->shouldBeCalled();
315     $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row2, $source, $migration])
316       ->willReturn([TRUE, TRUE])
317       ->shouldBeCalled();
318     $source->setModuleHandler($module_handler->reveal());
319     $this->assertTrue($source->prepareRow($row2->reveal()));
320   }
321
322   /**
323    * Test that global prepare hooks can skip rows.
324    *
325    * @covers ::prepareRow
326    */
327   public function testPrepareRowGlobalPrepareSkip() {
328     $this->migrationConfiguration['id'] = 'test_migration';
329
330     $migration = $this->getMigration();
331     $source = new StubSourcePlugin([], '', [], $migration);
332     $row = new Row();
333
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])
338       ->shouldBeCalled();
339     $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
340       ->willReturn([TRUE, TRUE])
341       ->shouldBeCalled();
342     $source->setModuleHandler($module_handler->reveal());
343
344     $this->idMap->expects($this->once())
345       ->method('saveIdMapping')
346       ->with($row, [], MigrateIdMapInterface::STATUS_IGNORED);
347
348     $this->assertFalse($source->prepareRow($row));
349   }
350
351   /**
352    * Test that migrate specific prepare hooks can skip rows.
353    *
354    * @covers ::prepareRow
355    */
356   public function testPrepareRowMigratePrepareSkip() {
357     $this->migrationConfiguration['id'] = 'test_migration';
358
359     $migration = $this->getMigration();
360     $source = new StubSourcePlugin([], '', [], $migration);
361     $row = new Row();
362
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])
367       ->shouldBeCalled();
368     $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
369       ->willReturn([TRUE, FALSE, TRUE])
370       ->shouldBeCalled();
371     $source->setModuleHandler($module_handler->reveal());
372
373     $this->idMap->expects($this->once())
374       ->method('saveIdMapping')
375       ->with($row, [], MigrateIdMapInterface::STATUS_IGNORED);
376
377     $this->assertFalse($source->prepareRow($row));
378   }
379
380   /**
381    * Test that a skip exception during prepare hooks correctly skips.
382    *
383    * @covers ::prepareRow
384    */
385   public function testPrepareRowPrepareException() {
386     $this->migrationConfiguration['id'] = 'test_migration';
387
388     $migration = $this->getMigration();
389     $source = new StubSourcePlugin([], '', [], $migration);
390     $row = new Row();
391
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])
396       ->shouldBeCalled();
397     $module_handler->invokeAll('migrate_' . $migration->id() . '_prepare_row', [$row, $source, $migration])
398       ->willThrow(new MigrateSkipRowException())
399       ->shouldBeCalled();
400     $source->setModuleHandler($module_handler->reveal());
401
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));
408
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])
412       ->willThrow($e)
413       ->shouldBeCalled();
414     $this->assertFalse($source->prepareRow($row));
415   }
416
417   /**
418    * Test that cacheCounts, skipCount, trackChanges preserve their default
419    * values.
420    */
421   public function testDefaultPropertiesValues() {
422     $this->migrationConfiguration['id'] = 'test_migration';
423     $migration = $this->getMigration();
424     $source = new StubSourceGeneratorPlugin([], '', [], $migration);
425
426     // Test the default value of the skipCount Value;
427     $this->assertTrue($source->getSkipCount());
428     $this->assertTrue($source->getCacheCounts());
429     $this->assertTrue($source->getTrackChanges());
430   }
431
432   /**
433    * Gets a mock executable for the test.
434    *
435    * @param \Drupal\migrate\Plugin\MigrationInterface $migration
436    *   The migration entity.
437    *
438    * @return \Drupal\migrate\MigrateExecutable
439    *   The migrate executable.
440    */
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);
447   }
448
449 }
450
451 /**
452  * Stubbed source plugin for testing base class implementations.
453  */
454 class StubSourcePlugin extends SourcePluginBase {
455
456   /**
457    * Helper for setting internal module handler implementation.
458    *
459    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
460    *   The module handler.
461    */
462   public function setModuleHandler(ModuleHandlerInterface $module_handler) {
463     $this->moduleHandler = $module_handler;
464   }
465
466   /**
467    * {@inheritdoc}
468    */
469   public function fields() {
470     return [];
471   }
472
473   /**
474    * {@inheritdoc}
475    */
476   public function __toString() {
477     return '';
478   }
479
480   /**
481    * {@inheritdoc}
482    */
483   public function getIds() {
484     return [];
485   }
486
487   /**
488    * {@inheritdoc}
489    */
490   protected function initializeIterator() {
491     return [];
492   }
493
494 }
495
496 /**
497  * Stubbed source plugin with a generator as iterator. Also it overwrites the
498  * $skipCount, $cacheCounts and $trackChanges properties.
499  */
500 class StubSourceGeneratorPlugin extends StubSourcePlugin {
501
502   /**
503    * {@inheritdoc}
504    */
505   protected $skipCount = TRUE;
506
507   /**
508    * {@inheritdoc}
509    */
510   protected $cacheCounts = TRUE;
511
512   /**
513    * {@inheritdoc}
514    */
515   protected $trackChanges = TRUE;
516
517   /**
518    * Return the skipCount value.
519    */
520   public function getSkipCount() {
521     return $this->skipCount;
522   }
523
524   /**
525    * Return the cacheCounts value.
526    */
527   public function getCacheCounts() {
528     return $this->cacheCounts;
529   }
530
531   /**
532    * Return the trackChanges value.
533    */
534   public function getTrackChanges() {
535     return $this->trackChanges;
536   }
537
538   /**
539    * {@inheritdoc}
540    */
541   protected function initializeIterator() {
542     $data = [
543       ['title' => 'foo'],
544       ['title' => 'bar'],
545       ['title' => 'iggy'],
546     ];
547     foreach ($data as $row) {
548       yield $row;
549     }
550   }
551
552 }