80c55ef13565f24ff443dfb276de73b58e5c4330
[yaffs-website] / web / core / modules / migrate / tests / src / Unit / MigrateSqlIdMapTest.php
1 <?php
2
3 namespace Drupal\Tests\migrate\Unit;
4
5 use Drupal\Core\Database\Driver\sqlite\Connection;
6 use Drupal\migrate\Plugin\MigrationInterface;
7 use Drupal\migrate\MigrateException;
8 use Drupal\migrate\Plugin\MigrateIdMapInterface;
9 use Drupal\migrate\Row;
10
11 /**
12  * Tests the SQL ID map plugin.
13  *
14  * @group migrate
15  */
16 class MigrateSqlIdMapTest extends MigrateTestCase {
17
18   /**
19    * The migration configuration, initialized to set the ID and destination IDs.
20    *
21    * @var array
22    */
23   protected $migrationConfiguration = [
24     'id' => 'sql_idmap_test',
25   ];
26
27   /**
28    * The source IDs.
29    *
30    * @var array
31    */
32   protected $sourceIds = [
33     'source_id_property' => [
34       'type' => 'string',
35     ],
36   ];
37
38   /**
39    * The destination IDs.
40    *
41    * @var array
42    */
43   protected $destinationIds = [
44     'destination_id_property' => [
45       'type' => 'string',
46     ],
47   ];
48
49   /**
50    * The database connection.
51    *
52    * @var \Drupal\Core\Database\Connection
53    */
54   protected $database;
55
56   /**
57    * {@inheritdoc}
58    */
59   protected function setUp() {
60     $this->database = $this->getDatabase([]);
61   }
62
63   /**
64    * Saves a single ID mapping row in the database.
65    *
66    * @param array $map
67    *   The row to save.
68    */
69   protected function saveMap(array $map) {
70     $table = 'migrate_map_sql_idmap_test';
71
72     $schema = $this->database->schema();
73     // If the table already exists, add any columns which are in the map array,
74     // but don't yet exist in the table. Yay, flexibility!
75     if ($schema->tableExists($table)) {
76       foreach (array_keys($map) as $field) {
77         if (!$schema->fieldExists($table, $field)) {
78           $schema->addField($table, $field, ['type' => 'text']);
79         }
80       }
81     }
82     else {
83       $schema->createTable($table, $this->createSchemaFromRow($map));
84     }
85
86     $this->database->insert($table)->fields($map)->execute();
87   }
88
89   /**
90    * Creates a test SQL ID map plugin.
91    *
92    * @return \Drupal\Tests\migrate\Unit\TestSqlIdMap
93    *   A SQL ID map plugin test instance.
94    */
95   protected function getIdMap() {
96     $migration = $this->getMigration();
97
98     $plugin = $this->getMock('Drupal\migrate\Plugin\MigrateSourceInterface');
99     $plugin
100       ->method('getIds')
101       ->willReturn($this->sourceIds);
102     $migration
103       ->method('getSourcePlugin')
104       ->willReturn($plugin);
105
106     $plugin = $this->getMock('Drupal\migrate\Plugin\MigrateDestinationInterface');
107     $plugin
108       ->method('getIds')
109       ->willReturn($this->destinationIds);
110     $migration
111       ->method('getDestinationPlugin')
112       ->willReturn($plugin);
113     $event_dispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
114
115     $id_map = new TestSqlIdMap($this->database, [], 'sql', [], $migration, $event_dispatcher);
116     $migration
117       ->method('getIdMap')
118       ->willReturn($id_map);
119
120     return $id_map;
121   }
122
123   /**
124    * Sets defaults for SQL ID map plugin tests.
125    *
126    * @return array
127    *   An associative array with the following keys:
128    *   - source_row_status
129    *   - rollback_action
130    *   - hash
131    */
132   protected function idMapDefaults() {
133     $defaults = [
134       'source_row_status' => MigrateIdMapInterface::STATUS_IMPORTED,
135       'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
136       'hash' => '',
137     ];
138     // By default, the PDO SQLite driver strongly prefers to return strings
139     // from SELECT queries. Even for columns that don't store strings. Even
140     // if the connection's STRINGIFY_FETCHES attribute is FALSE. This can cause
141     // assertSame() calls to fail, since 0 !== '0'. Casting these values to
142     // strings isn't the most elegant workaround, but it allows the assertions
143     // to pass properly.
144     if ($this->database->driver() == 'sqlite') {
145       $defaults['source_row_status'] = (string) $defaults['source_row_status'];
146       $defaults['rollback_action'] = (string) $defaults['rollback_action'];
147     }
148     return $defaults;
149   }
150
151   /**
152    * Tests the ID mapping method.
153    *
154    * Create two ID mappings and update the second to verify that:
155    * - saving new to empty tables work.
156    * - saving new to nonempty tables work.
157    * - updating work.
158    */
159   public function testSaveIdMapping() {
160     $source = [
161       'source_id_property' => 'source_value',
162     ];
163     $row = new Row($source, ['source_id_property' => []]);
164     $id_map = $this->getIdMap();
165     $id_map->saveIdMapping($row, ['destination_id_property' => 2]);
166     $expected_result = [
167       [
168         'sourceid1' => 'source_value',
169         'source_ids_hash' => $this->getIdMap()->getSourceIDsHash($source),
170         'destid1' => 2,
171       ] + $this->idMapDefaults(),
172     ];
173     $this->queryResultTest($this->getIdMapContents(), $expected_result);
174     $source = [
175       'source_id_property' => 'source_value_1',
176     ];
177     $row = new Row($source, ['source_id_property' => []]);
178     $id_map->saveIdMapping($row, ['destination_id_property' => 3]);
179     $expected_result[] = [
180       'sourceid1' => 'source_value_1',
181       'source_ids_hash' => $this->getIdMap()->getSourceIDsHash($source),
182       'destid1' => 3,
183     ] + $this->idMapDefaults();
184     $this->queryResultTest($this->getIdMapContents(), $expected_result);
185     $id_map->saveIdMapping($row, ['destination_id_property' => 4]);
186     $expected_result[1]['destid1'] = 4;
187     $this->queryResultTest($this->getIdMapContents(), $expected_result);
188   }
189
190   /**
191    * Tests the SQL ID map set message method.
192    */
193   public function testSetMessage() {
194     $message = $this->getMock('Drupal\migrate\MigrateMessageInterface');
195     $id_map = $this->getIdMap();
196     $id_map->setMessage($message);
197     $this->assertAttributeEquals($message, 'message', $id_map);
198   }
199
200   /**
201    * Tests the clear messages method.
202    */
203   public function testClearMessages() {
204     $message = 'Hello world.';
205     $expected_results = [0, 1, 2, 3];
206     $id_map = $this->getIdMap();
207
208     // Insert 4 message for later delete.
209     foreach ($expected_results as $key => $expected_result) {
210       $id_map->saveMessage(['source_id_property' => $key], $message);
211     }
212
213     // Truncate and check that 4 messages were deleted.
214     $this->assertSame($id_map->messageCount(), 4);
215     $id_map->clearMessages();
216     $count = $id_map->messageCount();
217     $this->assertSame($count, 0);
218   }
219
220   /**
221    * Tests the getRowsNeedingUpdate method for rows that need an update.
222    */
223   public function testGetRowsNeedingUpdate() {
224     $id_map = $this->getIdMap();
225     $row_statuses = [
226       MigrateIdMapInterface::STATUS_IMPORTED,
227       MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
228       MigrateIdMapInterface::STATUS_IGNORED,
229       MigrateIdMapInterface::STATUS_FAILED,
230     ];
231     // Create a mapping row for each STATUS constant.
232     foreach ($row_statuses as $status) {
233       $source = ['source_id_property' => 'source_value_' . $status];
234       $row = new Row($source, ['source_id_property' => []]);
235       $destination = ['destination_id_property' => 'destination_value_' . $status];
236       $id_map->saveIdMapping($row, $destination, $status);
237       $expected_results[] = [
238         'sourceid1' => 'source_value_' . $status,
239         'source_ids_hash' => $this->getIdMap()->getSourceIDsHash($source),
240         'destid1' => 'destination_value_' . $status,
241         'source_row_status' => $status,
242         'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
243         'hash' => '',
244       ];
245       // Assert zero rows need an update.
246       if ($status == MigrateIdMapInterface::STATUS_IMPORTED) {
247         $rows_needing_update = $id_map->getRowsNeedingUpdate(1);
248         $this->assertCount(0, $rows_needing_update);
249       }
250     }
251     // Assert that test values exist.
252     $this->queryResultTest($this->getIdMapContents(), $expected_results);
253
254     // Assert a single row needs an update.
255     $row_needing_update = $id_map->getRowsNeedingUpdate(1);
256     $this->assertCount(1, $row_needing_update);
257
258     // Assert the row matches its original source.
259     $source_id = $expected_results[MigrateIdMapInterface::STATUS_NEEDS_UPDATE]['sourceid1'];
260     $test_row = $id_map->getRowBySource(['source_id_property' => $source_id]);
261     // $row_needing_update is an array of objects returned from the database,
262     // but $test_row is an array, so the cast is necessary.
263     $this->assertSame($test_row, (array) $row_needing_update[0]);
264
265     // Add additional row that needs an update.
266     $source = ['source_id_property' => 'source_value_multiple'];
267     $row = new Row($source, ['source_id_property' => []]);
268     $destination = ['destination_id_property' => 'destination_value_multiple'];
269     $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
270
271     // Assert multiple rows need an update.
272     $rows_needing_update = $id_map->getRowsNeedingUpdate(2);
273     $this->assertCount(2, $rows_needing_update);
274   }
275
276   /**
277    * Tests the SQL ID map message count method by counting and saving messages.
278    */
279   public function testMessageCount() {
280     $message = 'Hello world.';
281     $expected_results = [0, 1, 2, 3];
282     $id_map = $this->getIdMap();
283
284     // Test count message multiple times starting from 0.
285     foreach ($expected_results as $key => $expected_result) {
286       $count = $id_map->messageCount();
287       $this->assertSame($expected_result, $count);
288       $id_map->saveMessage(['source_id_property' => $key], $message);
289     }
290   }
291
292   /**
293    * Tests the SQL ID map save message method.
294    */
295   public function testMessageSave() {
296     $message = 'Hello world.';
297     $original_values = [
298       1 => ['message' => $message, 'level' => MigrationInterface::MESSAGE_ERROR],
299       2 => ['message' => $message, 'level' => MigrationInterface::MESSAGE_WARNING],
300       3 => ['message' => $message, 'level' => MigrationInterface::MESSAGE_NOTICE],
301       4 => ['message' => $message, 'level' => MigrationInterface::MESSAGE_INFORMATIONAL],
302     ];
303     $expected_results = [
304       '7ad742edb7e866caa78ced1e4455d2e9cbd8adb2074e7c323d21b4e67732e755' => ['message' => $message, 'level' => MigrationInterface::MESSAGE_ERROR],
305       '2d3ec2b0c547e819346e6ae03f881fd9f5c978ff3cbe29dfb807d40735e53703' => ['message' => $message, 'level' => MigrationInterface::MESSAGE_WARNING],
306       '12a042f72cad9a2a8c7715df0c7695d762975f0687d87f5d480725dae1432a6f' => ['message' => $message, 'level' => MigrationInterface::MESSAGE_NOTICE],
307       'd9d1fd27a2447ace48f47a2e9ff649673f67b446d9381a7963c949fc083f8791' => ['message' => $message, 'level' => MigrationInterface::MESSAGE_INFORMATIONAL],
308     ];
309     $id_map = $this->getIdMap();
310
311     foreach ($original_values as $key => $original_value) {
312       $id_map->saveMessage(['source_id_property' => $key], $message, $original_value['level']);
313     }
314
315     foreach ($id_map->getMessageIterator() as $message_row) {
316       $key = $message_row->source_ids_hash;
317       $this->assertEquals($expected_results[$key]['message'], $message_row->message);
318       $this->assertEquals($expected_results[$key]['level'], $message_row->level);
319     }
320
321     // Insert with default level.
322     $message_default = 'Hello world default.';
323     $id_map->saveMessage(['source_id_property' => 5], $message_default);
324     $messages = $id_map->getMessageIterator(['source_id_property' => 5]);
325     $count = 0;
326     foreach ($messages as $key => $message_row) {
327       $count = 1;
328       $this->assertEquals($message_default, $message_row->message);
329       $this->assertEquals(MigrationInterface::MESSAGE_ERROR, $message_row->level);
330     }
331     $this->assertEquals($count, 1);
332
333     // Retrieve messages with a specific level.
334     $messages = $id_map->getMessageIterator([], MigrationInterface::MESSAGE_WARNING);
335     $count = 0;
336     foreach ($messages as $key => $message_row) {
337       $count = 1;
338       $this->assertEquals(MigrationInterface::MESSAGE_WARNING, $message_row->level);
339     }
340     $this->assertEquals($count, 1);
341   }
342
343   /**
344    * Tests the getRowBySource method.
345    */
346   public function testGetRowBySource() {
347     $this->getDatabase([]);
348     $row = [
349       'sourceid1' => 'source_id_value_1',
350       'sourceid2' => 'source_id_value_2',
351       'source_ids_hash' => $this->getIdMap()->getSourceIDsHash(['source_id_property' => 'source_id_value_1']),
352       'destid1' => 'destination_id_value_1',
353     ] + $this->idMapDefaults();
354     $this->saveMap($row);
355     $row = [
356       'sourceid1' => 'source_id_value_3',
357       'sourceid2' => 'source_id_value_4',
358       'source_ids_hash' => $this->getIdMap()->getSourceIDsHash(['source_id_property' => 'source_id_value_3', 'sourceid2' => 'source_id_value_4']),
359       'destid1' => 'destination_id_value_2',
360     ] + $this->idMapDefaults();
361     $this->saveMap($row);
362     $source_id_values = ['source_id_property' => $row['sourceid1'], 'sourceid2' => $row['sourceid2']];
363     $id_map = $this->getIdMap();
364     $result_row = $id_map->getRowBySource($source_id_values);
365     $this->assertSame($row, $result_row);
366     $source_id_values = ['source_id_property' => 'missing_value_1', 'sourceid2' => 'missing_value_2'];
367     $result_row = $id_map->getRowBySource($source_id_values);
368     $this->assertFalse($result_row);
369   }
370
371   /**
372    * Data provider for testLookupDestinationIdMapping().
373    *
374    * Scenarios to test (for both hits and misses) are:
375    * - Single-value source ID to single-value destination ID.
376    * - Multi-value source ID to multi-value destination ID.
377    * - Single-value source ID to multi-value destination ID.
378    * - Multi-value source ID to single-value destination ID.
379    *
380    * @return array
381    *   An array of data values.
382    */
383   public function lookupDestinationIdMappingDataProvider() {
384     return [
385       [1, 1],
386       [2, 2],
387       [1, 2],
388       [2, 1],
389     ];
390   }
391
392   /**
393    * Performs destination ID test on source and destination fields.
394    *
395    * @param int $num_source_fields
396    *   Number of source fields to test.
397    * @param int $num_destination_fields
398    *   Number of destination fields to test.
399    *
400    * @dataProvider lookupDestinationIdMappingDataProvider
401    */
402   public function testLookupDestinationIdMapping($num_source_fields, $num_destination_fields) {
403     // Adjust the migration configuration according to the number of source and
404     // destination fields.
405     $this->sourceIds = [];
406     $this->destinationIds = [];
407     $source_id_values = [];
408     $nonexistent_id_values = [];
409     $row = $this->idMapDefaults();
410     for ($i = 1; $i <= $num_source_fields; $i++) {
411       $row["sourceid$i"] = "source_id_value_$i";
412       $source_id_values[] = "source_id_value_$i";
413       $nonexistent_id_values[] = "nonexistent_source_id_value_$i";
414       $this->sourceIds["source_id_property_$i"] = [];
415     }
416     $expected_result = [];
417     for ($i = 1; $i <= $num_destination_fields; $i++) {
418       $row["destid$i"] = "destination_id_value_$i";
419       $expected_result[] = "destination_id_value_$i";
420       $this->destinationIds["destination_id_property_$i"] = [];
421     }
422     $row['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash($source_id_values);
423     $this->saveMap($row);
424     $id_map = $this->getIdMap();
425     // Test for a valid hit.
426     $destination_id = $id_map->lookupDestinationId($source_id_values);
427     $this->assertSame($expected_result, $destination_id);
428     // Test for a miss.
429     $destination_id = $id_map->lookupDestinationId($nonexistent_id_values);
430     $this->assertSame(0, count($destination_id));
431   }
432
433   /**
434    * Setup a database with the given rows.
435    *
436    * @param array $source_keys
437    *   The source keys for the ID map table.
438    * @param array $dest_keys
439    *   The destination keys for the ID map table.
440    * @param array $rows
441    *   An array of source and destination value arrays for the ID map table.
442    *
443    * @return \Drupal\Tests\migrate\Unit\TestSqlIdMap
444    *   An ID map instance for testing.
445    */
446   protected function setupRows($source_keys, $dest_keys, $rows) {
447     $this->database = $this->getDatabase([]);
448     $this->sourceIds = array_fill_keys($source_keys, []);
449     $this->destinationIds = array_fill_keys($dest_keys, []);
450
451     $db_keys = [];
452     foreach (array_keys($source_keys) as $i) {
453       $db_keys[] = 'sourceid' . ($i + 1);
454     }
455     foreach (array_keys($dest_keys) as $i) {
456       $db_keys[] = 'destid' . ($i + 1);
457     }
458     foreach ($rows as $row) {
459       $values = array_combine($db_keys, $row);
460       $source_values = array_slice($row, 0, count($source_keys));
461       $values['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash($source_values);
462       $this->saveMap($values);
463     }
464
465     return $this->getIdMap();
466   }
467
468   /**
469    * Tests lookupDestinationIds().
470    */
471   public function testLookupDestinationIds() {
472     // Simple map with one source and one destination ID.
473     $id_map = $this->setupRows(['nid'], ['nid'], [
474       [1, 101],
475       [2, 102],
476       [3, 103],
477     ]);
478
479     // Lookup nothing, gives nothing.
480     $this->assertEquals([], $id_map->lookupDestinationIds([]));
481     // Lookup by complete non-associative list.
482     $this->assertEquals([[101]], $id_map->lookupDestinationIds([1]));
483     $this->assertEquals([[102]], $id_map->lookupDestinationIds([2]));
484     $this->assertEquals([], $id_map->lookupDestinationIds([99]));
485     // Lookup by complete associative list.
486     $this->assertEquals([[101]], $id_map->lookupDestinationIds(['nid' => 1]));
487     $this->assertEquals([[102]], $id_map->lookupDestinationIds(['nid' => 2]));
488     $this->assertEquals([], $id_map->lookupDestinationIds(['nid' => 99]));
489
490     // Map with multiple source and destination IDs.
491     $id_map = $this->setupRows(['nid', 'language'], ['nid', 'langcode'], [
492       [1, 'en', 101, 'en'],
493       [1, 'fr', 101, 'fr'],
494       [1, 'de', 101, 'de'],
495       [2, 'en', 102, 'en'],
496     ]);
497
498     // Lookup nothing, gives nothing.
499     $this->assertEquals([], $id_map->lookupDestinationIds([]));
500     // Lookup by complete non-associative list.
501     $this->assertEquals([[101, 'en']], $id_map->lookupDestinationIds([1, 'en']));
502     $this->assertEquals([[101, 'fr']], $id_map->lookupDestinationIds([1, 'fr']));
503     $this->assertEquals([[102, 'en']], $id_map->lookupDestinationIds([2, 'en']));
504     $this->assertEquals([], $id_map->lookupDestinationIds([2, 'fr']));
505     $this->assertEquals([], $id_map->lookupDestinationIds([99, 'en']));
506     // Lookup by complete associative list.
507     $this->assertEquals([[101, 'en']], $id_map->lookupDestinationIds(['nid' => 1, 'language' => 'en']));
508     $this->assertEquals([[101, 'fr']], $id_map->lookupDestinationIds(['nid' => 1, 'language' => 'fr']));
509     $this->assertEquals([[102, 'en']], $id_map->lookupDestinationIds(['nid' => 2, 'language' => 'en']));
510     $this->assertEquals([], $id_map->lookupDestinationIds(['nid' => 2, 'language' => 'fr']));
511     $this->assertEquals([], $id_map->lookupDestinationIds(['nid' => 99, 'language' => 'en']));
512     // Lookup by partial non-associative list.
513     $this->assertEquals([[101, 'en'], [101, 'fr'], [101, 'de']], $id_map->lookupDestinationIds([1]));
514     $this->assertEquals([[102, 'en']], $id_map->lookupDestinationIds([2]));
515     $this->assertEquals([], $id_map->lookupDestinationIds([99]));
516     // Lookup by partial associative list.
517     $this->assertEquals([[101, 'en'], [101, 'fr'], [101, 'de']], $id_map->lookupDestinationIds(['nid' => 1]));
518     $this->assertEquals([[102, 'en']], $id_map->lookupDestinationIds(['nid' => 2]));
519     $this->assertEquals([], $id_map->lookupDestinationIds(['nid' => 99]));
520     // Out-of-order partial associative list.
521     $this->assertEquals([[101, 'en'], [102, 'en']], $id_map->lookupDestinationIds(['language' => 'en']));
522     $this->assertEquals([[101, 'fr']], $id_map->lookupDestinationIds(['language' => 'fr']));
523     $this->assertEquals([], $id_map->lookupDestinationIds(['language' => 'zh']));
524     // Error conditions.
525     try {
526       $id_map->lookupDestinationIds([1, 2, 3]);
527       $this->fail('Too many source IDs should throw');
528     }
529     catch (MigrateException $e) {
530       $this->assertEquals("Extra unknown items in source IDs", $e->getMessage());
531     }
532     try {
533       $id_map->lookupDestinationIds(['nid' => 1, 'aaa' => '2']);
534       $this->fail('Unknown source ID key should throw');
535     }
536     catch (MigrateException $e) {
537       $this->assertEquals("Extra unknown items in source IDs", $e->getMessage());
538     }
539
540     // Verify that we are looking up by source_id_hash when all source IDs are
541     // passed in.
542     $id_map->getDatabase()->update($id_map->mapTableName())
543       ->condition('sourceid1', 1)
544       ->condition('sourceid2', 'en')
545       ->fields([TestSqlIdMap::SOURCE_IDS_HASH => uniqid()])
546       ->execute();
547     $this->assertNotEquals([[101, 'en']], $id_map->lookupDestinationIds([1, 'en']));
548   }
549
550   /**
551    * Tests the getRowByDestination method.
552    */
553   public function testGetRowByDestination() {
554     $row = [
555       'sourceid1' => 'source_id_value_1',
556       'sourceid2' => 'source_id_value_2',
557       'source_ids_hash' => $this->getIdMap()->getSourceIDsHash(['source_id_property' => 'source_id_value_1']),
558       'destid1' => 'destination_id_value_1',
559     ] + $this->idMapDefaults();
560     $this->saveMap($row);
561     $row = [
562       'sourceid1' => 'source_id_value_3',
563       'sourceid2' => 'source_id_value_4',
564       'source_ids_hash' => $this->getIdMap()->getSourceIDsHash(['source_id_property' => 'source_id_value_3']),
565       'destid1' => 'destination_id_value_2',
566     ] + $this->idMapDefaults();
567     $this->saveMap($row);
568     $dest_id_values = ['destination_id_property' => $row['destid1']];
569     $id_map = $this->getIdMap();
570     $result_row = $id_map->getRowByDestination($dest_id_values);
571     $this->assertSame($row, $result_row);
572     // This value does not exist.
573     $dest_id_values = ['destination_id_property' => 'invalid_destination_id_property'];
574     $id_map = $this->getIdMap();
575     $result_row = $id_map->getRowByDestination($dest_id_values);
576     $this->assertFalse($result_row);
577   }
578
579   /**
580    * Data provider for testLookupSourceIDMapping().
581    *
582    * Scenarios to test (for both hits and misses) are:
583    * - Single-value destination ID to single-value source ID.
584    * - Multi-value destination ID to multi-value source ID.
585    * - Single-value destination ID to multi-value source ID.
586    * - Multi-value destination ID to single-value source ID.
587    *
588    * @return array
589    *   An array of data values.
590    */
591   public function lookupSourceIDMappingDataProvider() {
592     return [
593       [1, 1],
594       [2, 2],
595       [1, 2],
596       [2, 1],
597     ];
598   }
599
600   /**
601    * Performs the source ID test on source and destination fields.
602    *
603    * @param int $num_source_fields
604    *   Number of source fields to test.
605    * @param int $num_destination_fields
606    *   Number of destination fields to test.
607    *
608    * @dataProvider lookupSourceIDMappingDataProvider
609    */
610   public function testLookupSourceIDMapping($num_source_fields, $num_destination_fields) {
611     // Adjust the migration configuration according to the number of source and
612     // destination fields.
613     $this->sourceIds = [];
614     $this->destinationIds = [];
615     $row = $this->idMapDefaults();
616     $source_ids_values = [];
617     $expected_result = [];
618     for ($i = 1; $i <= $num_source_fields; $i++) {
619       $row["sourceid$i"] = "source_id_value_$i";
620       $source_ids_values = [$row["sourceid$i"]];
621       $expected_result["source_id_property_$i"] = "source_id_value_$i";
622       $this->sourceIds["source_id_property_$i"] = [];
623     }
624     $destination_id_values = [];
625     $nonexistent_id_values = [];
626     for ($i = 1; $i <= $num_destination_fields; $i++) {
627       $row["destid$i"] = "destination_id_value_$i";
628       $destination_id_values["destination_id_property_$i"] = "destination_id_value_$i";
629       $nonexistent_id_values["destination_id_property_$i"] = "nonexistent_destination_id_value_$i";
630       $this->destinationIds["destination_id_property_$i"] = [];
631     }
632     $row['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash($source_ids_values);
633     $this->saveMap($row);
634     $id_map = $this->getIdMap();
635     // Test for a valid hit.
636     $source_id = $id_map->lookupSourceID($destination_id_values);
637     $this->assertSame($expected_result, $source_id);
638     // Test for a miss.
639     $source_id = $id_map->lookupSourceID($nonexistent_id_values);
640     $this->assertSame(0, count($source_id));
641   }
642
643   /**
644    * Tests currentDestination() and currentSource().
645    */
646   public function testCurrentDestinationAndSource() {
647     // Simple map with one source and one destination ID.
648     $id_map = $this->setupRows(['nid'], ['nid'], [
649       [1, 101],
650       [2, 102],
651       [3, 103],
652       // Mock a failed row by setting the destination ID to NULL.
653       [4, NULL],
654     ]);
655
656     // The rows are ordered by destination ID so the failed row should be first.
657     $id_map->rewind();
658     $this->assertEquals([], $id_map->currentDestination());
659     $this->assertEquals(['nid' => 4], $id_map->currentSource());
660     $id_map->next();
661     $this->assertEquals(['nid' => 101], $id_map->currentDestination());
662     $this->assertEquals(['nid' => 1], $id_map->currentSource());
663     $id_map->next();
664     $this->assertEquals(['nid' => 102], $id_map->currentDestination());
665     $this->assertEquals(['nid' => 2], $id_map->currentSource());
666     $id_map->next();
667     $this->assertEquals(['nid' => 103], $id_map->currentDestination());
668     $this->assertEquals(['nid' => 3], $id_map->currentSource());
669     $id_map->next();
670   }
671
672   /**
673    * Tests the imported count method.
674    *
675    * Scenarios to test for:
676    * - No imports.
677    * - One import.
678    * - Multiple imports.
679    */
680   public function testImportedCount() {
681     $id_map = $this->getIdMap();
682     // Add a single failed row and assert zero imported rows.
683     $source = ['source_id_property' => 'source_value_failed'];
684     $row = new Row($source, ['source_id_property' => []]);
685     $destination = ['destination_id_property' => 'destination_value_failed'];
686     $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_FAILED);
687     $this->assertSame(0, $id_map->importedCount());
688
689     // Add an imported row and assert single count.
690     $source = ['source_id_property' => 'source_value_imported'];
691     $row = new Row($source, ['source_id_property' => []]);
692     $destination = ['destination_id_property' => 'destination_value_imported'];
693     $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_IMPORTED);
694     $this->assertSame(1, $id_map->importedCount());
695
696     // Add a row needing update and assert multiple imported rows.
697     $source = ['source_id_property' => 'source_value_update'];
698     $row = new Row($source, ['source_id_property' => []]);
699     $destination = ['destination_id_property' => 'destination_value_update'];
700     $id_map->saveIdMapping($row, $destination, MigrateIdMapInterface::STATUS_NEEDS_UPDATE);
701     $this->assertSame(2, $id_map->importedCount());
702   }
703
704   /**
705    * Tests the number of processed source rows.
706    *
707    * Scenarios to test for:
708    * - No processed rows.
709    * - One processed row.
710    * - Multiple processed rows.
711    */
712   public function testProcessedCount() {
713     $id_map = $this->getIdMap();
714     // Assert zero rows have been processed before adding rows.
715     $this->assertSame(0, $id_map->processedCount());
716     $row_statuses = [
717       MigrateIdMapInterface::STATUS_IMPORTED,
718       MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
719       MigrateIdMapInterface::STATUS_IGNORED,
720       MigrateIdMapInterface::STATUS_FAILED,
721     ];
722     // Create a mapping row for each STATUS constant.
723     foreach ($row_statuses as $status) {
724       $source = ['source_id_property' => 'source_value_' . $status];
725       $row = new Row($source, ['source_id_property' => []]);
726       $destination = ['destination_id_property' => 'destination_value_' . $status];
727       $id_map->saveIdMapping($row, $destination, $status);
728       if ($status == MigrateIdMapInterface::STATUS_IMPORTED) {
729         // Assert a single row has been processed.
730         $this->assertSame(1, $id_map->processedCount());
731       }
732     }
733     // Assert multiple rows have been processed.
734     $this->assertSame(count($row_statuses), $id_map->processedCount());
735   }
736
737   /**
738    * Data provider for testUpdateCount().
739    *
740    * Scenarios to test for:
741    * - No updates.
742    * - One update.
743    * - Multiple updates.
744    *
745    * @return array
746    *   An array of data values.
747    */
748   public function updateCountDataProvider() {
749     return [
750       [0],
751       [1],
752       [3],
753     ];
754   }
755
756   /**
757    * Performs the update count test with a given number of update rows.
758    *
759    * @param int $num_update_rows
760    *   The number of update rows to test.
761    *
762    * @dataProvider updateCountDataProvider
763    */
764   public function testUpdateCount($num_update_rows) {
765     for ($i = 0; $i < 5; $i++) {
766       $row = $this->idMapDefaults();
767       $row['sourceid1'] = "source_id_value_$i";
768       $row['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash(['source_id_property' => $row['sourceid1']]);
769       $row['destid1'] = "destination_id_value_$i";
770       $row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
771       $this->saveMap($row);
772     }
773     for (; $i < 5 + $num_update_rows; $i++) {
774       $row = $this->idMapDefaults();
775       $row['sourceid1'] = "source_id_value_$i";
776       $row['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash(['source_id_property' => $row['sourceid1']]);
777       $row['destid1'] = "destination_id_value_$i";
778       $row['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE;
779       $this->saveMap($row);
780     }
781     $id_map = $this->getIdMap();
782     $this->assertSame($num_update_rows, $id_map->updateCount());
783   }
784
785   /**
786    * Data provider for testErrorCount().
787    *
788    * Scenarios to test for:
789    * - No errors.
790    * - One error.
791    * - Multiple errors.
792    *
793    * @return array
794    *   An array of data values.
795    */
796   public function errorCountDataProvider() {
797     return [
798       [0],
799       [1],
800       [3],
801     ];
802   }
803
804   /**
805    * Performs error count test with a given number of error rows.
806    *
807    * @param int $num_error_rows
808    *   Number of error rows to test.
809    *
810    * @dataProvider errorCountDataProvider
811    */
812   public function testErrorCount($num_error_rows) {
813     for ($i = 0; $i < 5; $i++) {
814       $row = $this->idMapDefaults();
815       $row['sourceid1'] = "source_id_value_$i";
816       $row['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash(['source_id_property' => $row['sourceid1']]);
817       $row['destid1'] = "destination_id_value_$i";
818       $row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
819       $this->saveMap($row);
820     }
821     for (; $i < 5 + $num_error_rows; $i++) {
822       $row = $this->idMapDefaults();
823       $row['sourceid1'] = "source_id_value_$i";
824       $row['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash(['source_id_property' => $row['sourceid1']]);
825       $row['destid1'] = "destination_id_value_$i";
826       $row['source_row_status'] = MigrateIdMapInterface::STATUS_FAILED;
827       $this->saveMap($row);
828     }
829
830     $this->assertSame($num_error_rows, $this->getIdMap()->errorCount());
831   }
832
833   /**
834    * Tests setting a row source_row_status to STATUS_NEEDS_UPDATE.
835    */
836   public function testSetUpdate() {
837     $id_map = $this->getIdMap();
838     $row_statuses = [
839       MigrateIdMapInterface::STATUS_IMPORTED,
840       MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
841       MigrateIdMapInterface::STATUS_IGNORED,
842       MigrateIdMapInterface::STATUS_FAILED,
843     ];
844     // Create a mapping row for each STATUS constant.
845     foreach ($row_statuses as $status) {
846       $source = ['source_id_property' => 'source_value_' . $status];
847       $row = new Row($source, ['source_id_property' => []]);
848       $destination = ['destination_id_property' => 'destination_value_' . $status];
849       $id_map->saveIdMapping($row, $destination, $status);
850       $expected_results[] = [
851         'sourceid1' => 'source_value_' . $status,
852         'source_ids_hash' => $this->getIdMap()->getSourceIDsHash($source),
853         'destid1' => 'destination_value_' . $status,
854         'source_row_status' => $status,
855         'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
856         'hash' => '',
857       ];
858     }
859     // Assert that test values exist.
860     $this->queryResultTest($this->getIdMapContents(), $expected_results);
861     // Mark each row as STATUS_NEEDS_UPDATE.
862     foreach ($row_statuses as $status) {
863       $id_map->setUpdate(['source_id_property' => 'source_value_' . $status]);
864     }
865     // Update expected results.
866     foreach ($expected_results as $key => $value) {
867       $expected_results[$key]['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE;
868     }
869     // Assert that updated expected values match.
870     $this->queryResultTest($this->getIdMapContents(), $expected_results);
871     // Assert an exception is thrown when source identifiers are not provided.
872     try {
873       $id_map->setUpdate([]);
874       $this->assertFalse(FALSE, 'MigrateException not thrown, when source identifiers were provided to update.');
875     }
876     catch (MigrateException $e) {
877       $this->assertTrue(TRUE, "MigrateException thrown, when source identifiers were not provided to update.");
878     }
879   }
880
881   /**
882    * Tests prepareUpdate().
883    */
884   public function testPrepareUpdate() {
885     $id_map = $this->getIdMap();
886     $row_statuses = [
887       MigrateIdMapInterface::STATUS_IMPORTED,
888       MigrateIdMapInterface::STATUS_NEEDS_UPDATE,
889       MigrateIdMapInterface::STATUS_IGNORED,
890       MigrateIdMapInterface::STATUS_FAILED,
891     ];
892
893     // Create a mapping row for each STATUS constant.
894     foreach ($row_statuses as $status) {
895       $source = ['source_id_property' => 'source_value_' . $status];
896       $row = new Row($source, ['source_id_property' => []]);
897       $destination = ['destination_id_property' => 'destination_value_' . $status];
898       $id_map->saveIdMapping($row, $destination, $status);
899       $expected_results[] = [
900         'sourceid1' => 'source_value_' . $status,
901         'destid1' => 'destination_value_' . $status,
902         'source_row_status' => $status,
903         'rollback_action' => MigrateIdMapInterface::ROLLBACK_DELETE,
904         'hash' => '',
905       ];
906     }
907
908     // Assert that test values exist.
909     $this->queryResultTest($this->getIdMapContents(), $expected_results);
910
911     // Mark all rows as STATUS_NEEDS_UPDATE.
912     $id_map->prepareUpdate();
913
914     // Update expected results.
915     foreach ($expected_results as $key => $value) {
916       $expected_results[$key]['source_row_status'] = MigrateIdMapInterface::STATUS_NEEDS_UPDATE;
917     }
918     // Assert that updated expected values match.
919     $this->queryResultTest($this->getIdMapContents(), $expected_results);
920   }
921
922   /**
923    * Tests the destroy method.
924    *
925    * Scenarios to test for:
926    * - No errors.
927    * - One error.
928    * - Multiple errors.
929    */
930   public function testDestroy() {
931     $id_map = $this->getIdMap();
932     // Initialize the ID map.
933     $id_map->getDatabase();
934     $map_table_name = $id_map->mapTableName();
935     $message_table_name = $id_map->messageTableName();
936     $row = new Row(['source_id_property' => 'source_value'], ['source_id_property' => []]);
937     $id_map->saveIdMapping($row, ['destination_id_property' => 2]);
938     $id_map->saveMessage(['source_id_property' => 'source_value'], 'A message');
939     $this->assertTrue($this->database->schema()->tableExists($map_table_name),
940                       "$map_table_name exists");
941     $this->assertTrue($this->database->schema()->tableExists($message_table_name),
942                       "$message_table_name exists");
943     $id_map->destroy();
944     $this->assertFalse($this->database->schema()->tableExists($map_table_name),
945                        "$map_table_name does not exist");
946     $this->assertFalse($this->database->schema()->tableExists($message_table_name),
947                        "$message_table_name does not exist");
948   }
949
950   /**
951    * Tests the getQualifiedMapTable method with a prefixed database.
952    */
953   public function testGetQualifiedMapTablePrefix() {
954     $connection_options = [
955       'database' => ':memory:',
956       'prefix' => 'prefix',
957     ];
958     $pdo = Connection::open($connection_options);
959     $this->database = new Connection($pdo, $connection_options);
960     $qualified_map_table = $this->getIdMap()->getQualifiedMapTableName();
961     // The SQLite driver is a special flower. It will prefix tables with
962     // PREFIX.TABLE, instead of the standard PREFIXTABLE.
963     // @see \Drupal\Core\Database\Driver\sqlite\Connection::__construct()
964     $this->assertEquals('prefix.migrate_map_sql_idmap_test', $qualified_map_table);
965   }
966
967   /**
968    * Tests all the iterator methods in one swing.
969    *
970    * The iterator methods are:
971    * - Sql::rewind()
972    * - Sql::next()
973    * - Sql::valid()
974    * - Sql::key()
975    * - Sql::current()
976    */
977   public function testIterators() {
978     for ($i = 0; $i < 3; $i++) {
979       $row = $this->idMapDefaults();
980       $row['sourceid1'] = "source_id_value_$i";
981       $row['source_ids_hash'] = $this->getIdMap()->getSourceIDsHash(['source_id_property' => $row['sourceid1']]);
982       $row['destid1'] = "destination_id_value_$i";
983       $row['source_row_status'] = MigrateIdMapInterface::STATUS_IMPORTED;
984       $expected_results[serialize(['sourceid1' => $row['sourceid1']])] = ['destid1' => $row['destid1']];
985       $this->saveMap($row);
986     }
987
988     $this->assertSame(iterator_to_array($this->getIdMap()), $expected_results);
989   }
990
991   /**
992    * Retrieves the contents of an ID map.
993    *
994    * @return array
995    *   The contents of an ID map.
996    */
997   private function getIdMapContents() {
998     $result = $this->database
999       ->select('migrate_map_sql_idmap_test', 't')
1000       ->fields('t')
1001       ->execute();
1002
1003     // The return value needs to be countable, or it will fail certain
1004     // assertions. iterator_to_array() will not suffice because it won't
1005     // respect the PDO fetch mode, if specified.
1006     $contents = [];
1007     foreach ($result as $row) {
1008       $contents[] = (array) $row;
1009     }
1010     return $contents;
1011   }
1012
1013 }