Chris@0: migration = $this->prophesize(MigrationInterface::class); Chris@0: Chris@0: $this->migration->id()->willReturn( Chris@0: $this->randomMachineName(16) Chris@0: ); Chris@0: // Prophesize a useless ID map plugin and an empty set of destination IDs. Chris@0: // Calling code can override these prophecies later and set up different Chris@0: // behaviors. Chris@0: $this->migration->getIdMap()->willReturn( Chris@0: $this->prophesize(MigrateIdMapInterface::class)->reveal() Chris@0: ); Chris@0: $this->migration->getDestinationIds()->willReturn([]); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tests a negative batch size throws an exception. Chris@0: */ Chris@0: public function testBatchSizeNegative() { Chris@0: $this->setExpectedException(MigrateException::class, 'batch_size must be greater than or equal to zero'); Chris@0: $plugin = $this->getPlugin(['batch_size' => -1]); Chris@0: $plugin->next(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tests a non integer batch size throws an exception. Chris@0: */ Chris@0: public function testBatchSizeNonInteger() { Chris@0: $this->setExpectedException(MigrateException::class, 'batch_size must be greater than or equal to zero'); Chris@0: $plugin = $this->getPlugin(['batch_size' => '1']); Chris@0: $plugin->next(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function queryDataProvider() { Chris@0: // Define the parameters for building the data array. The first element is Chris@0: // the number of source data rows, the second is the batch size to set on Chris@0: // the plugin configuration. Chris@0: $test_parameters = [ Chris@0: // Test when batch size is 0. Chris@0: [200, 0], Chris@0: // Test when rows mod batch size is 0. Chris@0: [200, 20], Chris@0: // Test when rows mod batch size is > 0. Chris@0: [200, 30], Chris@0: // Test when batch size = row count. Chris@0: [200, 200], Chris@0: // Test when batch size > row count. Chris@0: [200, 300], Chris@0: ]; Chris@0: Chris@0: // Build the data provider array. The provider array consists of the source Chris@0: // data rows, the expected result data, the expected count, the plugin Chris@0: // configuration, the expected batch size and the expected batch count. Chris@0: $table = 'query_batch_test'; Chris@0: $tests = []; Chris@0: $data_set = 0; Chris@0: foreach ($test_parameters as $data) { Chris@0: list($num_rows, $batch_size) = $data; Chris@0: for ($i = 0; $i < $num_rows; $i++) { Chris@0: $tests[$data_set]['source_data'][$table][] = [ Chris@0: 'id' => $i, Chris@0: 'data' => $this->randomString(), Chris@0: ]; Chris@0: } Chris@0: $tests[$data_set]['expected_data'] = $tests[$data_set]['source_data'][$table]; Chris@0: $tests[$data_set][2] = $num_rows; Chris@0: // Plugin configuration array. Chris@0: $tests[$data_set][3] = ['batch_size' => $batch_size]; Chris@0: // Expected batch size. Chris@0: $tests[$data_set][4] = $batch_size; Chris@0: // Expected batch count is 0 unless a batch size is set. Chris@0: $expected_batch_count = 0; Chris@0: if ($batch_size > 0) { Chris@0: $expected_batch_count = (int) ($num_rows / $batch_size); Chris@0: if ($num_rows % $batch_size) { Chris@0: // If there is a remainder an extra batch is needed to get the Chris@0: // remaining rows. Chris@0: $expected_batch_count++; Chris@0: } Chris@0: } Chris@0: $tests[$data_set][5] = $expected_batch_count; Chris@0: $data_set++; Chris@0: } Chris@0: return $tests; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Tests query batch size. Chris@0: * Chris@0: * @param array $source_data Chris@0: * The source data, keyed by table name. Each table is an array containing Chris@0: * the rows in that table. Chris@0: * @param array $expected_data Chris@0: * The result rows the plugin is expected to return. Chris@0: * @param int $num_rows Chris@0: * How many rows the source plugin is expected to return. Chris@0: * @param array $configuration Chris@0: * Configuration for the source plugin specifying the batch size. Chris@0: * @param int $expected_batch_size Chris@0: * The expected batch size, will be set to zero for invalid batch sizes. Chris@0: * @param int $expected_batch_count Chris@0: * The total number of batches. Chris@0: * Chris@0: * @dataProvider queryDataProvider Chris@0: */ Chris@0: public function testQueryBatch($source_data, $expected_data, $num_rows, $configuration, $expected_batch_size, $expected_batch_count) { Chris@0: $plugin = $this->getPlugin($configuration); Chris@0: Chris@0: // Since we don't yet inject the database connection, we need to use a Chris@0: // reflection hack to set it in the plugin instance. Chris@0: $reflector = new \ReflectionObject($plugin); Chris@0: $property = $reflector->getProperty('database'); Chris@0: $property->setAccessible(TRUE); Chris@0: Chris@0: $connection = $this->getDatabase($source_data); Chris@0: $property->setValue($plugin, $connection); Chris@0: Chris@0: // Test the results. Chris@0: $i = 0; Chris@0: /** @var \Drupal\migrate\Row $row */ Chris@0: foreach ($plugin as $row) { Chris@0: Chris@0: $expected = $expected_data[$i++]; Chris@0: $actual = $row->getSource(); Chris@0: Chris@0: foreach ($expected as $key => $value) { Chris@0: $this->assertArrayHasKey($key, $actual); Chris@0: $this->assertSame((string) $value, (string) $actual[$key]); Chris@0: } Chris@0: } Chris@0: Chris@0: // Test that all rows were retrieved. Chris@0: self::assertSame($num_rows, $i); Chris@0: Chris@0: // Test the batch size. Chris@0: if (is_null($expected_batch_size)) { Chris@0: $expected_batch_size = $configuration['batch_size']; Chris@0: } Chris@0: $property = $reflector->getProperty('batchSize'); Chris@0: $property->setAccessible(TRUE); Chris@0: self::assertSame($expected_batch_size, $property->getValue($plugin)); Chris@0: Chris@0: // Test the batch count. Chris@0: if (is_null($expected_batch_count)) { Chris@0: $expected_batch_count = intdiv($num_rows, $expected_batch_size); Chris@0: if ($num_rows % $configuration['batch_size']) { Chris@0: $expected_batch_count++; Chris@0: } Chris@0: } Chris@0: $property = $reflector->getProperty('batch'); Chris@0: $property->setAccessible(TRUE); Chris@0: self::assertSame($expected_batch_count, $property->getValue($plugin)); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Instantiates the source plugin under test. Chris@0: * Chris@0: * @param array $configuration Chris@0: * The source plugin configuration. Chris@0: * Chris@0: * @return \Drupal\migrate\Plugin\MigrateSourceInterface|object Chris@0: * The fully configured source plugin. Chris@0: */ Chris@0: protected function getPlugin($configuration) { Chris@0: /** @var \Drupal\migrate\Plugin\MigratePluginManager $plugin_manager */ Chris@0: $plugin_manager = $this->container->get('plugin.manager.migrate.source'); Chris@0: $plugin = $plugin_manager->createInstance('query_batch_test', $configuration, $this->migration->reveal()); Chris@0: Chris@0: $this->migration Chris@0: ->getSourcePlugin() Chris@0: ->willReturn($plugin); Chris@0: return $plugin; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Builds an in-memory SQLite database from a set of source data. Chris@0: * Chris@0: * @param array $source_data Chris@0: * The source data, keyed by table name. Each table is an array containing Chris@0: * the rows in that table. Chris@0: * Chris@0: * @return \Drupal\Core\Database\Driver\sqlite\Connection Chris@0: * The SQLite database connection. Chris@0: */ Chris@0: protected function getDatabase(array $source_data) { Chris@0: // Create an in-memory SQLite database. Plugins can interact with it like Chris@0: // any other database, and it will cease to exist when the connection is Chris@0: // closed. Chris@0: $connection_options = ['database' => ':memory:']; Chris@0: $pdo = Connection::open($connection_options); Chris@0: $connection = new Connection($pdo, $connection_options); Chris@0: Chris@0: // Create the tables and fill them with data. Chris@0: foreach ($source_data as $table => $rows) { Chris@0: // Use the biggest row to build the table schema. Chris@0: $counts = array_map('count', $rows); Chris@0: asort($counts); Chris@0: end($counts); Chris@0: $pilot = $rows[key($counts)]; Chris@0: Chris@0: $connection->schema() Chris@0: ->createTable($table, [ Chris@0: // SQLite uses loose affinity typing, so it's OK for every field to Chris@0: // be a text field. Chris@0: 'fields' => array_map(function () { Chris@0: return ['type' => 'text']; Chris@0: }, $pilot), Chris@0: ]); Chris@0: Chris@0: $fields = array_keys($pilot); Chris@0: $insert = $connection->insert($table)->fields($fields); Chris@0: array_walk($rows, [$insert, 'values']); Chris@0: $insert->execute(); Chris@0: } Chris@0: return $connection; Chris@0: } Chris@0: Chris@0: }