Commit a4a1418c authored by Mike Ryan's avatar Mike Ryan

#37: Add console tests.

parent 7d476736
Pipeline #58447949 passed with stage
in 2 minutes and 22 seconds
......@@ -12,6 +12,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- The `Filter` interface has been added, to determine whether a DataRecord should be processed.
- The `Select` filter has been added, allowing for filtering by comparing DataRecord properties to values using PHP comparison operators.
- The `--select` option has been added to the `migrate` command, allow for ad-hoc filtering of extracted data at runtime.
- Added basic console command tests.
## [0.5.3] - 2019-04-12
......
<?php
declare(strict_types=1);
namespace Soong\Loader;
use Soong\Contracts\Data\DataRecord;
/**
* Loader for testing/debugging pipelines.
*/
class PrintR extends LoaderBase
{
/**
* @inheritdoc
*/
public function load(DataRecord $data) : void
{
print_r($data);
}
/**
* @inheritdoc
*/
public function getKeyProperties(): array
{
return [];
}
/**
* @inheritdoc
*/
public function getProperties(): array
{
return [];
}
/**
* @inheritdoc
*/
public function delete(array $key) : void
{
// @todo not supported
}
}
......@@ -74,9 +74,16 @@ class EtlTask extends Task implements EtlTaskInterface
// Each expression arrives in the form "$name$op$value" = we need to
// turn that into an array [$name, $op, $value].
$criteria = [];
$operatorList = implode('|', Select::OPERATORS);
// Note that if '=' is before '==' in the operator array, 'a==b'
// will be parsed as 'a', '=', '=b'. To prevent this, make sure the
// operators are sorted longest first.
$operatorList = Select::OPERATORS;
usort($operatorList, function ($a, $b) {
return $b <=> $a;
});
$operatorExpression = implode('|', $operatorList);
foreach ($options['select'] as $expression) {
if (!preg_match("/(.*?)($operatorList)(.*)/", $expression, $matches)) {
if (!preg_match("/(.*?)($operatorExpression)(.*)/", $expression, $matches)) {
// @todo Throw exception - should be Command exception.
}
$criteria[] = [$matches[1], $matches[2], $matches[3]];
......@@ -216,10 +223,10 @@ class EtlTask extends Task implements EtlTaskInterface
}
}
$loader->load($resultData);
// @todo Handle multi-column keys.
$extractedKey = $data->getProperty(array_keys($extractor->getKeyProperties())[0])->getValue();
$loadedKey = $resultData->getProperty(array_keys($loader->getKeyProperties())[0])->getValue();
if (isset($keyMap)) {
// @todo Handle multi-column keys.
$extractedKey = $data->getProperty(array_keys($extractor->getKeyProperties())[0])->getValue();
$loadedKey = $resultData->getProperty(array_keys($loader->getKeyProperties())[0])->getValue();
$keyMap->saveKeyMap([$extractedKey], [$loadedKey]);
}
}
......
<?php
namespace Soong\Tests\Console;
use PHPUnit\Framework\TestCase;
use Soong\Data\Record;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
/**
* Tests the \Soong\Console\Coommand\MigrateCommand class.
*/
abstract class CommandTestBase extends TestCase
{
/**
* Fully qualified name of the command class to test.
*
* @var string
*/
protected $commandClass;
/**
* Command name being tested.
*
* @var string
*/
protected $commandName;
/**
* Command testing object.
*
* @var CommandTester
*/
protected $commandTester;
/**
* @inheritdoc
*/
public function setUp(): void
{
parent::setUp();
$application = new Application('Soong POC', '0.1.0');
$application->add(new $this->commandClass());
$command = $application->find($this->commandName);
$this->commandTester = new CommandTester($command);
}
}
<?php
namespace Soong\Tests\Console;
use Soong\Data\Record;
/**
* Tests the \Soong\Console\Command\MigrateCommand class.
*/
class MigrateCommandTest extends CommandTestBase
{
/**
* @var string
*/
protected $commandClass = '\Soong\Console\Command\MigrateCommand';
/**
* @var string
*/
protected $commandName = 'migrate';
/**
* Provides data for testing the migrate console command.
*
* @return array
* List of data sets, each containing:
* An array of command options.
* The expected output generated directly by the migration process.
* The expected output generated by the command.
*/
public function commandDataProvider() : array
{
$row1 = new Record([
'id' => 1,
'foo' => 'first record',
]);
$row2 = new Record([
'id' => 5,
'foo' => 'second record',
]);
$expectedMigrationOutput = print_r($row1, true) . print_r($row2, true);
$data['basic migration'] = [
[
'tasks' => ['test1'],
'--directory' => ['tests/test_config_1'],
],
$expectedMigrationOutput,
"Executing test1\n",
];
$row1 = new Record([
'id' => 1,
'foo' => 'first record',
]);
$row2 = new Record([
'id' => 5,
'foo' => 'second record',
]);
$expectedMigrationOutput = print_r($row2, true);
$data['select option'] = [
[
'tasks' => ['test1'],
'--directory' => ['tests/test_config_1'],
'--select' => ['id==5'],
],
$expectedMigrationOutput,
"Executing test1\n",
];
$row3 = new Record([
'blah' => 89,
'blahblah' => 'Ba-lue Bolivar',
]);
$row4 = new Record([
'blah' => 3821,
'blahblah' => 'Ba-lues-are',
]);
$expectedMigrationOutput = print_r($row1, true) . print_r($row2, true) .
print_r($row3, true) . print_r($row4, true);
$data['multiple dirs/tasks'] = [
[
'tasks' => ['test1', 'test2'],
'--directory' => ['tests/test_config_1', 'tests/test_config_2'],
],
$expectedMigrationOutput,
"Executing test1\nExecuting test2\n",
];
return $data;
}
/**
* Test the execution of one migrate command with given options.
*
* @dataProvider commandDataProvider
*
* @param $commandOptions
* Options to be passed to the command.
* @param $expectedMigrationOutput
* Output expected from the migration process itself.
* @param $expectedCommandOutput
* Output expected from the command implemention.
*/
public function testCommand($commandOptions, $expectedMigrationOutput, $expectedCommandOutput) : void
{
$this->commandTester->execute($commandOptions);
$this->expectOutputString($expectedMigrationOutput);
$commandOutput = $this->commandTester->getDisplay();
$this->assertEquals($expectedCommandOutput, $commandOutput);
}
}
<?php
namespace Soong\Tests\Console;
use Soong\Data\Record;
/**
* Tests the \Soong\Console\Command\RollbackCommand class.
*/
class RollbackCommandTest extends CommandTestBase
{
/**
* @var string
*/
protected $commandClass = '\Soong\Console\Command\RollbackCommand';
/**
* @var string
*/
protected $commandName = 'rollback';
/**
* @inheritdoc
*/
public function setUp(): void
{
parent::setUp();
$data[] = new Record([
'id' => 1,
'foo' => 'first record',
]);
$data[] = new Record([
'id' => 5,
'foo' => 'second record',
]);
file_put_contents('/tmp/testrollback.data', serialize($data));
$map = [
'7ad742edb7e866caa78ced1e4455d2e9cbd8adb2074e7c323d21b4e67732e755' => [
[1],
[1],
],
'a49314150d4e4ecf744ef984324881fa713f4df38b647d154a8cd0c634fa395e' => [
[5],
[5],
],
];
file_put_contents('/tmp/testrollback.keymap', serialize($map));
}
/**
* @inheritdoc
*/
public function tearDown(): void
{
parent::tearDown();
unlink('/tmp/testrollback.data');
unlink('/tmp/testrollback.keymap');
}
/**
* Provides data for testing the rollback console command.
*
* @return array
* List of data sets, each containing:
* An array of command options.
* The expected data resulting from the rollback process.
* The expected output generated by the command.
*/
public function commandDataProvider() : array
{
$data['basic rollback'] = [
[
'tasks' => ['testrollback'],
'--directory' => ['tests/test_config_1'],
],
'a:0:{}',
"Executing testrollback\n",
];
return $data;
}
/**
* Test the execution of one rollback command with given options.
*
* @dataProvider commandDataProvider
*
* @param $commandOptions
* Options to be passed to the command.
* @param $expectedDataResult
* Output expected from the migration process itself.
* @param $expectedCommandOutput
* Output expected from the command implemention.
*/
public function testCommand($commandOptions, $expectedDataResult, $expectedCommandOutput) : void
{
$this->commandTester->execute($commandOptions);
$commandOutput = $this->commandTester->getDisplay();
$this->assertEquals($expectedCommandOutput, $commandOutput);
$dataResult = file_get_contents('/tmp/testrollback.data');
$this->assertEquals($expectedDataResult, $dataResult, 'Data has been rolled back.');
$mapResult = file_get_contents('/tmp/testrollback.keymap');
$this->assertEquals($expectedDataResult, $mapResult, 'Map has been rolled back.');
}
}
<?php
declare(strict_types=1);
namespace Soong\Tests\Console;
use Soong\KeyMap\KeyMapBase;
/**
* Simple key map implementation using serialize().
*/
class SerializeKeyMap extends KeyMapBase
{
use SerializeTrait;
/**
* @inheritdoc
*/
protected function optionDefinitions(): array
{
$options = parent::optionDefinitions();
$options['file'] = [
'required' => true,
'allowed_types' => 'string',
];
return $options;
}
/**
* @inheritdoc
*/
public function saveKeyMap(array $extractedKey, array $loadedKey) : void
{
$extractedKey = array_values($extractedKey);
$loadedKey = array_values($loadedKey);
$keyMap = $this->getData();
$keyMap[$this->hashKeys($extractedKey)] = [$extractedKey, $loadedKey];
$this->putData($keyMap);
}
/**
* @inheritdoc
*/
public function lookupLoadedKey(array $extractedKey) : array
{
// Loaded key array is the second element of the key map entry.
return $this->getData()[$this->hashKeys($extractedKey)][1];
}
/**
* @inheritdoc
*/
public function lookupExtractedKeys(array $loadedKey) : array
{
foreach ($this->getData() as $keyMapEntry) {
[$mapExtractedKey, $mapLoadedKey] = $keyMapEntry;
if ($loadedKey == $mapLoadedKey) {
return $mapExtractedKey;
}
}
return [];
}
/**
* @inheritdoc
*/
public function delete(array $extractedKey) : void
{
$keyMap = $this->getData();
unset($keyMap[$this->hashKeys($extractedKey)]);
$this->putData($keyMap);
}
/**
* @inheritdoc
*/
public function count() : int
{
return count($this->getData());
}
/**
* @inheritdoc
*/
public function iterate() : iterable
{
foreach ($this->getData() as $keyValues) {
// Extracted keys are the first array member.
yield $keyValues[0];
}
}
}
<?php
declare(strict_types=1);
namespace Soong\Tests\Console;
use Soong\Contracts\Data\DataRecord;
use Soong\Loader\LoaderBase;
/**
* Simple loader for testing with persistence.
*/
class SerializeLoader extends LoaderBase
{
use SerializeTrait;
/**
* @inheritdoc
*/
protected function optionDefinitions(): array
{
$options = parent::optionDefinitions();
$options['file'] = [
'required' => true,
'allowed_types' => 'string',
];
return $options;
}
/**
* @inheritdoc
*/
public function load(DataRecord $data) : void
{
$completeData = $this->getData();
$completeData[] = $data;
$this->putData($completeData);
}
/**
* @inheritdoc
*/
public function delete(array $loadedKey) : void
{
$completeData = $this->getData();
/** @var \Soong\Contracts\Data\DataRecord $data */
foreach ($completeData as $index => $data) {
if ($loadedKey == [$data->getProperty('id')->getValue()]) {
unset($completeData[$index]);
}
}
$this->putData($completeData);
}
}
<?php
declare(strict_types=1);
namespace Soong\Tests\Console;
trait SerializeTrait
{
/**
* Serialize the data and save to the configured file.
*
* @param mixed $data
*/
protected function putData($data) : void
{
file_put_contents($this->getConfigurationValue('file'), serialize($data));
}
/**
* Retrieve the data from the configured file.
*
* @return array
*/
protected function getData() : array
{
if ($dataSerialized = @file_get_contents($this->getConfigurationValue('file'))) {
return unserialize($dataSerialized);
} else {
return [];
}
}
}
<?php
namespace Soong\Tests\Console;
/**
* Tests the \Soong\Console\Command\StatusCommand class.
*/
class StatusCommandTest extends CommandTestBase
{
/**
* @var string
*/
protected $commandClass = '\Soong\Console\Command\StatusCommand';
/**
* @var string
*/
protected $commandName = 'status';
/**
* Provides data for testing the status console command.
*
* @return array
* List of data sets, each containing:
* An array of command options.
* The expected output generated by the command.
*/
public function commandDataProvider() : array
{
$output = <<<'OUTPUT'
+--------------+-------+-----------+-------------+
| Task | Total | Processed | Unprocessed |
+--------------+-------+-----------+-------------+
| test1 | 2 | N/A | N/A |
| testrollback | 2 | 0 | 2 |
+--------------+-------+-----------+-------------+
OUTPUT;
$data['single directory'] = [
[
'--directory' => ['tests/test_config_1'],
],
$output,
];
$output = <<<'OUTPUT'
+--------------+-------+-----------+-------------+
| Task | Total | Processed | Unprocessed |
+--------------+-------+-----------+-------------+
| test1 | 2 | N/A | N/A |
| testrollback | 2 | 0 | 2 |
| test2 | 2 | N/A | N/A |
+--------------+-------+-----------+-------------+
OUTPUT;
$data['multiple directories'] = [
[
'--directory' => ['tests/test_config_1', 'tests/test_config_2'],
],
$output,
];
$output = <<<'OUTPUT'
+-------+-------+-----------+-------------+
| Task | Total | Processed | Unprocessed |
+-------+-------+-----------+-------------+
| test2 | 2 | N/A | N/A |
+-------+-------+-----------+-------------+
OUTPUT;
$data['specific task'] = [
[
'tasks' => ['test2'],
'--directory' => ['tests/test_config_1', 'tests/test_config_2'],
],
$output,