Skip to content
Snippets Groups Projects
Unverified Commit 715349a6 authored by Thomas Flori's avatar Thomas Flori
Browse files

implement and test migrateTo and revertTo

parent f601d23e
No related branches found
No related tags found
No related merge requests found
......@@ -52,4 +52,41 @@ class FileHelper
return $class ? substr($namespace, 1) . '\\' . $class : null;
}
/**
* Read the time in the filename of $path
*
* Required is only the date (the time 00:00Z is presumed) in ISO format (YYYY-MM-DD).
*
* The date may be followed by the time with or without a timezone (time zone Z is presumed). We don't recommend
* to use colons in time - for this we allow ':', '.' or '-' as dividers in time and time zone.
*
* Returns the timestamp (of course in UTC!)
*
* @param string $path
* @return int|null
*/
public static function getTimeFromFileName(string $path): ?int
{
static $regex;
if (!$regex) {
$regex = '/' .
'(\d{4}-[01]\d-[0-3]\d)' . // the date has to be given
'(?>(?> |T)' . // optional subgroup of time requires a divider
'([0-2]\d[:.-][0-5]\d(?>[:.-][0-5]\d)?)' . // time requires hh:mm optional with :ss
'(Z|[+-][0-2]\d(?>[:.-][0-5]\d)?)?' . // optional subgroup of time zone
')?' . // end subgroup of time
'/i';
}
$fileName = pathinfo($path, PATHINFO_FILENAME);
if (!preg_match($regex, $fileName, $match)) {
return null;
}
return strtotime(
$match[1] . 'T' .
(isset($match[2]) ? str_replace(['-', '.'], ':', $match[2]) : '00:00') .
(isset($match[3]) ? str_replace(['-', '.'], ':', $match[3]) : 'Z')
);
}
}
......@@ -70,37 +70,46 @@ class Migrations
public function migrate(): bool
{
$this->loadMigrations();
/** @var Model\Migration[] $migrations */
$migrations = array_filter($this->getStatus()->migrations, function (Model\Migration $migration) {
$migrations = array_filter($this->migrations, function (Model\Migration $migration) {
return $migration->status !== 'done';
});
return $this->up(...$migrations);
}
// public function migrateTo(string $file)
// {
// $found = false;
// $migrations = [];
// foreach ($this->getStatus()->migrations as $migration) {
// $migrations[] = $migration;
// if (strpos($migration, $file) !== false) {
// $found = true;
// break;
// }
// }
//
// if (!$found) {
// throw new \LogicException('No migration found matching ' . $file);
// }
//
// /** @var Model\Migration[] $migrations */
// $migrations = array_filter($migrations, function (Model\Migration $migration) {
// return $migration->status !== 'done';
// });
//
// return $this->up(...$migrations);
// }
public function migrateTo(string $file)
{
$this->loadMigrations();
$found = false;
$migrations = [];
foreach ($this->migrations as $migration) {
$migrations[] = $migration;
if (strpos($migration->file, $file) !== false) {
$found = true;
break;
}
}
if (!$found && $time = strtotime($file)) {
$migrations = array_filter($this->migrations, function (Model\Migration $migration) use ($time) {
$migrationTime = FileHelper::getTimeFromFileName($migration->file);
return is_null($migrationTime) || $migrationTime <= $time;
});
} elseif (!$found) {
throw new \LogicException('No migration found matching ' . $file);
}
/** @var Model\Migration[] $migrations */
$migrations = array_filter($migrations, function (Model\Migration $migration) {
return $migration->status !== 'done';
});
return $this->up(...$migrations);
}
public function up(Model\Migration ...$migrations)
{
......@@ -128,42 +137,48 @@ class Migrations
public function revert()
{
$this->loadMigrations();
/** @var Model\Migration[] $migrations */
$migrations = array_filter($this->getStatus()->migrations, function (Model\Migration $migration) {
$migrations = array_filter($this->migrations, function (Model\Migration $migration) {
return $migration->status === 'done' && !self::isInternal($migration->file);
});
return $this->down(...array_reverse($migrations));
}
// public function revertTo(string $file)
// {
// $status = $this->getStatus();
// $found = false;
// $migrations = [];
// foreach (array_reverse($status->migrations) as $migration) {
// $migrations[] = $migration;
// if (strpos($migration, $file) !== false) {
// $found = true;
// break;
// }
// }
//
// if (!$found) {
// throw new \LogicException('No migration found matching ' . $file);
// }
//
// /** @var Model\Migration[] $toExecute */
// $toExecute = array_filter($status->migrations, function (Model\Migration $migration) {
// return $migration->status === 'done' && !self::isInternal($migration);
// });
//
// foreach ($toExecute as $migration) {
// $this->down($migration);
// }
//
// return true;
// }
public function revertTo(string $file)
{
$this->loadMigrations();
$found = false;
$migrations = [];
foreach (array_reverse($this->migrations) as $migration) {
if (strpos($migration->file, $file) !== false) {
$found = true;
break;
}
$migrations[] = $migration;
}
if (!$found && $time = strtotime($file)) {
$migrations = array_reverse(
array_filter($this->migrations, function (Model\Migration $migration) use ($time) {
$migrationTime = FileHelper::getTimeFromFileName($migration->file);
return !is_null($migrationTime) && $migrationTime > $time;
})
);
} elseif (!$found) {
throw new \LogicException('No migration found matching ' . $file);
}
/** @var Model\Migration[] $toExecute */
$migrations = array_filter($migrations, function (Model\Migration $migration) {
return $migration->status === 'done' && !self::isInternal($migration->file);
});
return $this->down(...$migrations);
}
public function down(Model\Migration ...$migrations)
{
......@@ -294,26 +309,15 @@ class Migrations
$rightBaseName = basename($right->file);
// sort criteria 2: has creation date
$leftHasCreationDate = (int)preg_match(
'/^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z)_/',
$leftBaseName,
$leftCreationDate
);
$rightHasCreationDate = (int)preg_match(
'/^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z)_/',
$rightBaseName,
$rightCreationDate
);
if ($leftHasCreationDate !== $rightHasCreationDate) {
return $leftHasCreationDate - $rightHasCreationDate;
$leftTime = FileHelper::getTimeFromFileName($left->file);
$rightTime = FileHelper::getTimeFromFileName($right->file);
if (is_null($leftTime) !== is_null($rightTime)) {
return is_null($rightTime) - is_null($leftTime);
}
// sort criteria 3: by creation date
if (@$leftCreationDate[1] !== @$rightCreationDate[1]) {
list($leftDate, $leftTime) = explode('T', $leftCreationDate[1]);
list($rightDate, $rightTime) = explode('T', $rightCreationDate[1]);
list($leftTime, $rightTime) = str_replace('-', ':', [$leftTime, $rightTime]);
return strtotime($leftDate . 'T' . $leftTime) - strtotime($rightDate . 'T' . $rightTime);
if ($leftTime !== $rightTime) {
return $leftTime - $rightTime;
}
// sort criteria 4: alphabetically
......
......@@ -10,10 +10,10 @@ class MigrateTest extends TestCase
/** @test */
public function returnsSuccessWhenNoMigrationsNeedToBeExecuted()
{
$this->mockStatus(Migration::createInstance([
$this->setProtectedProperty($this->migrations, 'migrations', [Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'done'
]));
])]);
$this->migrations->shouldReceive('up')->with()->once()->andReturn(true);
$result = $this->migrations->migrate();
......@@ -24,10 +24,10 @@ class MigrateTest extends TestCase
/** @test */
public function executesNewMigrations()
{
$this->mockStatus($migration = Migration::createInstance([
$this->setProtectedProperty($this->migrations, 'migrations', [$migration = Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'new',
]));
])]);
$this->migrations->shouldReceive('up')->with($migration)->once()->andReturn(true);
$result = $this->migrations->migrate();
......@@ -38,11 +38,11 @@ class MigrateTest extends TestCase
/** @test */
public function executesFailedMigrations()
{
$this->mockStatus($migration = Migration::createInstance([
$this->setProtectedProperty($this->migrations, 'migrations', [$migration = Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'failed',
'executed' => date('c', strtotime('-1 hour')),
]));
])]);
$this->migrations->shouldReceive('up')->with($migration)->once()->andReturn(true);
$result = $this->migrations->migrate();
......@@ -53,16 +53,118 @@ class MigrateTest extends TestCase
/** @test */
public function executesRevertedMigrations()
{
$this->mockStatus($migration = Migration::createInstance([
$this->setProtectedProperty($this->migrations, 'migrations', [$migration = Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'reverted',
'executed' => date('c', strtotime('-1 hour')),
'reverted' => date('c', strtotime('-10 minutes')),
]));
])]);
$this->migrations->shouldReceive('up')->with($migration)->once()->andReturn(true);
$result = $this->migrations->migrate();
self::assertTrue($result);
}
/** @test */
public function migratesToMatchingFile()
{
$migrations = [
Migration::createInstance([
'file' => 'FileA.php',
'status' => 'new',
]),
Migration::createInstance([
'file' => 'FileB.php',
'status' => 'new',
]),
Migration::createInstance([
'file' => 'FileC.php',
'status' => 'new',
]),
];
$this->setProtectedProperty($this->migrations, 'migrations', $migrations);
$this->migrations->shouldReceive('up')->withArgs(array_slice($migrations, 0, 2))
->once()->andReturn(true);
$result = $this->migrations->migrateTo('FileB');
self::assertTrue($result);
}
/** @test */
public function throwsWhenNoFileMatches()
{
$migrations = [
Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'new',
]),
];
$this->setProtectedProperty($this->migrations, 'migrations', $migrations);
self::expectException(\LogicException::class);
self::expectExceptionMessage('No migration found matching FileB');
$this->migrations->migrateTo('FileB');
}
/** @test */
public function filtersByFilesBeforeOrEqualTime()
{
$migrations = [
Migration::createInstance([
'file' => 'WithoutTime.php',
'status' => 'new',
]),
Migration::createInstance([
'file' => '2018-01-01T00.00.00Z Before.php',
'status' => 'new',
]),
Migration::createInstance([
'file' => '2018-01-02T00.00.00Z Equal.php',
'status' => 'new',
]),
Migration::createInstance([
'file' => '2018-01-03T00.00.00Z After.php',
'status' => 'new',
]),
];
$this->setProtectedProperty($this->migrations, 'migrations', $migrations);
$this->migrations->shouldReceive('up')->withArgs(array_slice($migrations, 0, 3))
->once()->andReturn(true);
$result = $this->migrations->migrateTo('2018-01-02T00:00:00Z');
self::assertTrue($result);
}
/** @test */
public function filtersDoneAfterSearch()
{
$migrations = [
Migration::createInstance([
'file' => 'FileA.php',
'status' => 'done',
]),
Migration::createInstance([
'file' => 'FileB.php',
'status' => 'done',
]),
Migration::createInstance([
'file' => 'FileC.php',
'status' => 'new',
]),
];
$this->setProtectedProperty($this->migrations, 'migrations', $migrations);
$this->migrations->shouldReceive('up')->with()
->once()->andReturn(true);
$result = $this->migrations->migrateTo('FileB');
self::assertTrue($result);
}
}
......@@ -10,10 +10,10 @@ class RevertTest extends TestCase
/** @test */
public function returnsSuccessWhenNoMigrationsNeedToBeReverted()
{
$this->mockStatus(Migration::createInstance([
$this->setProtectedProperty($this->migrations, 'migrations', [Migration::createInstance([
'file' => 'CreateAnimalsTable.php',
'status' => 'new'
]));
])]);
$this->migrations->shouldReceive('down')->with()->once()->andReturn(true);
$result = $this->migrations->revert();
......@@ -24,10 +24,10 @@ class RevertTest extends TestCase
/** @test */
public function revertsDoneMigrations()
{
$this->mockStatus($migration = Migration::createInstance([
$this->setProtectedProperty($this->migrations, 'migrations', [$migration = Migration::createInstance([
'file' => 'CreateAnimalsTable.php',
'status' => 'done',
]));
])]);
$this->migrations->shouldReceive('down')->with($migration)->once()->andReturn(true);
$result = $this->migrations->revert();
......@@ -38,10 +38,10 @@ class RevertTest extends TestCase
/** @test */
public function doesNotRevertInternalMigrations()
{
$this->mockStatus(Migration::createInstance([
$this->setProtectedProperty($this->migrations, 'migrations', [Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'done'
]));
])]);
$this->migrations->shouldReceive('down')->with()->once()->andReturn(true);
$result = $this->migrations->revert();
......@@ -52,10 +52,10 @@ class RevertTest extends TestCase
/** @test */
public function doesNotRevertFailedMigrations()
{
$this->mockStatus(Migration::createInstance([
$this->setProtectedProperty($this->migrations, 'migrations', [Migration::createInstance([
'file' => 'CreateAnimalsTable.php',
'status' => 'failed'
]));
])]);
$this->migrations->shouldReceive('down')->with()->once()->andReturn(true);
$result = $this->migrations->revert();
......@@ -66,14 +66,128 @@ class RevertTest extends TestCase
/** @test */
public function doesNotRevertRevertedMigrations()
{
$this->mockStatus(Migration::createInstance([
$this->setProtectedProperty($this->migrations, 'migrations', [Migration::createInstance([
'file' => 'CreateAnimalsTable.php',
'status' => 'reverted'
]));
])]);
$this->migrations->shouldReceive('down')->with()->once()->andReturn(true);
$result = $this->migrations->revert();
self::assertTrue($result);
}
/** @test */
public function revertsToMatchingFile()
{
$migrations = [
Migration::createInstance([
'file' => 'FileA.php',
'status' => 'done',
]),
Migration::createInstance([
'file' => 'FileB.php',
'status' => 'done',
]),
Migration::createInstance([
'file' => 'FileC.php',
'status' => 'done',
]),
Migration::createInstance([
'file' => 'FileD.php',
'status' => 'done',
]),
];
$this->setProtectedProperty($this->migrations, 'migrations', $migrations);
$this->migrations->shouldReceive('down')->withArgs(array_reverse(array_slice($migrations, -2)))
->once()->andReturn(true);
$result = $this->migrations->revertTo('FileB');
self::assertTrue($result);
}
/** @test */
public function throwsWhenNoFileMatches()
{
$migrations = [
Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'done',
]),
];
$this->setProtectedProperty($this->migrations, 'migrations', $migrations);
self::expectException(\LogicException::class);
self::expectExceptionMessage('No migration found matching FileB');
$this->migrations->revertTo('FileB');
}
/** @test */
public function filtersByFilesAfterTime()
{
$migrations = [
Migration::createInstance([
'file' => 'WithoutTime.php',
'status' => 'done',
]),
Migration::createInstance([
'file' => '2018-01-01T00.00.00Z Before.php',
'status' => 'done',
]),
Migration::createInstance([
'file' => '2018-01-02T00.00.00Z Equal.php',
'status' => 'done',
]),
Migration::createInstance([
'file' => '2018-01-03T00.00.00Z After.php',
'status' => 'done',
]),
Migration::createInstance([
'file' => '2018-01-04T00.00.00Z After.php',
'status' => 'done',
]),
];
$this->setProtectedProperty($this->migrations, 'migrations', $migrations);
$this->migrations->shouldReceive('down')->withArgs(array_reverse(array_slice($migrations, -2)))
->once()->andReturn(true);
$result = $this->migrations->revertTo('2018-01-02T00:00:00Z');
self::assertTrue($result);
}
/** @test */
public function filtersNotDoneAfterSearch()
{
$migrations = [
Migration::createInstance([
'file' => 'FileA.php',
'status' => 'failed',
]),
Migration::createInstance([
'file' => 'FileB.php',
'status' => 'new',
]),
Migration::createInstance([
'file' => 'FileC.php',
'status' => 'done',
]),
Migration::createInstance([
'file' => 'FileD.php',
'status' => 'done',
]),
];
$this->setProtectedProperty($this->migrations, 'migrations', $migrations);
$this->migrations->shouldReceive('down')->withArgs(array_reverse(array_slice($migrations, -2)))
->once()->andReturn(true);
$result = $this->migrations->revertTo('FileA');
self::assertTrue($result);
}
}
......@@ -69,18 +69,6 @@ abstract class TestCase extends MockeryTestCase
return $statement;
}
protected function mockStatus(Migration ...$migrations): m\CompositeExpectation
{
$status = (object)[
'migrations' => $migrations,
'count' => count(array_filter($migrations, function (Migration $migration) {
return $migration->status !== 'done';
}))
];
return $this->migrations->shouldReceive('getStatus')->with()->andReturn($status);
}
protected function mockMigration(string $file, string $class, string $status = 'new'): \stdClass
{
return $this->mockMigrations(['file' => $file, 'class' => $class, 'status' => $status])[0];
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment