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

split migrate and up and implement revert and down

parent f5c8043d
No related branches found
No related tags found
No related merge requests found
......@@ -34,10 +34,8 @@ class FileHelper
for ($j = $i + 1; $j < count($tokens); $j++) {
if ($tokens[$j][0] === T_STRING) {
$namespace .= '\\' . $tokens[$j][1];
} else {
if ($tokens[$j] === '{' || $tokens[$j] === ';') {
break;
}
} elseif ($tokens[$j] === '{' || $tokens[$j] === ';') {
break;
}
}
}
......@@ -52,6 +50,6 @@ class FileHelper
}
}
return $class ? $namespace . '\\' . $class : null;
return $class ? substr($namespace, 1) . '\\' . $class : null;
}
}
......@@ -11,6 +11,7 @@ class CreateMigrationTable extends AbstractMigration
$this->exec("CREATE TABLE migrations (
file CHARACTER VARYING (64) NOT NULL,
executed TIMESTAMP NOT NULL,
reverted TIMESTAMP,
status CHARACTER VARYING (16) NOT NULL DEFAULT 'done',
statements TEXT,
executionTime DOUBLE PRECISION,
......@@ -21,8 +22,9 @@ class CreateMigrationTable extends AbstractMigration
$this->exec("CREATE INDEX migrations_execution_time ON migrations (executionTime)");
}
/** @codeCoverageIgnore */
public function down(): void
{
$this->exec('DROP TABLE migrations');
// we never delete this table again ;-)
}
}
......@@ -7,6 +7,7 @@ use Breyta\Model;
class Migrations
{
const INTERNAL_PREFIX = '@breyta/';
/** @var \PDO */
protected $db;
......@@ -16,9 +17,6 @@ class Migrations
/** @var array|Model\Migration[] */
protected $migrations;
/** @var array|String[] */
protected $classes;
/** @var array|Model\Migration[] */
protected $missingMigrations = [];
......@@ -70,26 +68,52 @@ class Migrations
return $status;
}
public function migrate(string $file = null): bool
public function migrate(): bool
{
$status = $this->getStatus();
$toExecute = array_filter($status->migrations, function (Model\Migration $migration) use ($file) {
return $migration->status !== 'done' && (!$file || strpos($migration->file, $file) !== false);
/** @var Model\Migration[] $migrations */
$migrations = array_filter($this->getStatus()->migrations, function (Model\Migration $migration) {
return $migration->status !== 'done';
});
/**
* @var string $file
* @var Model\Migration $migration
*/
foreach ($toExecute as $file => $migration) {
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 up(Model\Migration ...$migrations)
{
foreach ($migrations as $migration) {
$this->statements = [];
$start = microtime(true);
try {
$this->db->beginTransaction();
$class = self::internalClass($migration->file) ??
FileHelper::getClassFromFile($this->path . DIRECTORY_SEPARATOR . $migration->file);
/** @var AbstractMigration $migrationInstance */
$migrationInstance = call_user_func($this->resolver, $this->classes[$file], $this->getAdapter());
$migrationInstance = call_user_func($this->resolver, $class, $this->getAdapter());
$migrationInstance->up();
$this->saveMigration($migration, 'done', microtime(true) - $start);
$this->db->commit();
} catch (\PDOException $exception) {
......@@ -102,32 +126,108 @@ class Migrations
return true;
}
public function revert()
{
/** @var Model\Migration[] $migrations */
$migrations = array_filter($this->getStatus()->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 down(Model\Migration ...$migrations)
{
foreach ($migrations as $migration) {
$this->statements = $migration->statements;
$start = microtime(true) - $migration->executionTime;
try {
$this->db->beginTransaction();
$class = self::internalClass($migration->file) ??
FileHelper::getClassFromFile($this->path . DIRECTORY_SEPARATOR . $migration->file);
/** @var AbstractMigration $migrationInstance */
$migrationInstance = call_user_func($this->resolver, $class, $this->getAdapter());
$migrationInstance->down();
$this->saveMigration($migration, 'reverted', microtime(true) - $start);
$this->db->commit();
} catch (\PDOException $exception) {
$this->db->rollBack();
throw $exception;
}
}
return true;
}
protected function saveMigration(Model\Migration $migration, $status, $executionTime)
{
// delete it first (if there is an old line it is outdated)
$this->db->prepare("DELETE FROM migrations WHERE file = ?")->execute([$migration->file]);
$exists = (bool)$migration->executed;
$now = new \DateTime('now', new \DateTimeZone('UTC'));
$status === 'reverted' ? $migration->reverted = $now : $migration->executed = $now;
$migration->executed = new \DateTime('now', new \DateTimeZone('UTC'));
$migration->statements = $this->statements;
$migration->status = $status;
$migration->executionTime = $executionTime;
$this->db->prepare("INSERT INTO migrations
(file, executed, status, statements, executionTime) VALUES
(?, ?, ?, ?, ?)
")->execute([
$migration->file,
$migration->executed->format('c'),
$migration->status,
json_encode($migration->statements),
$migration->executionTime
]);
if (!$exists) {
$this->db->prepare("INSERT INTO migrations
(file, executed, status, statements, executionTime) VALUES
(?, ?, ?, ?, ?)
")->execute([
$migration->file,
$migration->executed->format('c'),
$migration->status,
json_encode($migration->statements),
$migration->executionTime
]);
} else {
$this->db->prepare("UPDATE migrations SET
executed = ?, reverted = ?, status = ?, statements = ?, executionTime = ?
WHERE file = ?
")->execute([
$migration->executed->format('c'),
$migration->reverted ? $migration->reverted->format('c') : null,
$migration->status,
json_encode($migration->statements),
$migration->executionTime,
$migration->file
]);
}
}
protected function loadMigrations()
{
if (!$this->migrations) {
$this->migrations = $this->findMigrations();
$migrations = $this->findMigrations();
// get the status of migrations from database
try {
......@@ -135,24 +235,24 @@ class Migrations
if ($statement) {
$statement->setFetchMode(\PDO::FETCH_CLASS, Model\Migration::class);
while ($migration = $statement->fetch()) {
if (!isset($this->migrations[$migration->file])) {
if (!isset($migrations[$migration->file])) {
$this->missingMigrations[] = $migration;
continue;
}
$this->migrations[$migration->file] = $migration;
$migrations[$migration->file] = $migration;
}
}
} catch (\PDOException $exception) {
// the table does not exist - so nothing to do here
}
$this->migrations = array_values($migrations);
}
}
protected function findMigrations(): array
{
$this->classes['@breyta/CreateMigrationTable.php'] = CreateMigrationTable::class;
$migrations = [Model\Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'file' => self::INTERNAL_PREFIX . 'CreateMigrationTable.php',
'status' => 'new',
])];
......@@ -169,13 +269,13 @@ class Migrations
if (!$className) {
continue;
}
require_once $fileInfo->getPathname();
if (!is_subclass_of($className, AbstractMigration::class)) {
continue;
}
$file = substr($fileInfo->getPathname(), strlen($this->path) + 1);
$this->classes[$file] = $className;
$migrations[] = Model\Migration::createInstance([
'file' => $file,
'status' => 'new'
......@@ -257,4 +357,14 @@ class Migrations
return $this->adapter;
}
protected static function internalClass(string $file): ?string
{
return self::isInternal($file) ? 'Breyta\\Migration\\' . substr($file, 8, -4) : null;
}
protected static function isInternal(string $file): bool
{
return strncmp($file, self::INTERNAL_PREFIX, strlen(self::INTERNAL_PREFIX)) === 0;
}
}
......@@ -13,11 +13,14 @@ class Migration
/** @var \DateTime */
public $executed;
/** @var \DateTime */
public $reverted;
/** @var string */
public $status;
/** @var string|array|Statement[] */
public $statements;
public $statements = [];
/** @var double */
public $executionTime;
......@@ -28,6 +31,10 @@ class Migration
$this->executed = new \DateTime($this->executed, new \DateTimeZone('UTC'));
}
if (!empty($this->reverted) && is_string($this->reverted)) {
$this->reverted = new \DateTime($this->reverted, new \DateTimeZone('UTC'));
}
if (!empty($this->statements) && is_string($this->statements)) {
$this->statements = array_map(function ($data) {
return Statement::createInstance($data);
......
......@@ -38,15 +38,4 @@ class CreateMigrationTableTest extends TestCase
->with(m::pattern('/create index.* on migrations\s*\(\s*executionTime\s*\)/i'))
->once();
}
/** @test */
public function dropsTheMigrationTable()
{
$adapter = m::mock(BasicAdapter::class)->shouldIgnoreMissing();
$migration = new CreateMigrationTable($adapter);
$migration->down();
$adapter->shouldHaveReceived('exec')->with(m::pattern('/drop table migrations/i'));
}
}
<?php
namespace Breyta\Test\Migrations;
use Breyta\Migration\CreateMigrationTable;
use Breyta\Model\Migration;
use Breyta\Model\Statement;
use Breyta\Test\Example\CreateAnimalsTable;
use Breyta\Test\TestCase;
class DownTest extends TestCase
{
/** @test */
public function executesMigrationsInSeparateTransactions()
{
list($migrationTableMigration, $animalsTableMigration) = $this->mockMigrations([
'file' => '@breyta/CreateMigrationTable.php',
'class' => CreateMigrationTable::class,
'status' => 'done'
], [
'file' => 'CreateAnimalsTable.php',
'class' => CreateAnimalsTable::class,
'status' => 'done'
]);
$this->pdo->shouldReceive('beginTransaction')->once()->ordered();
$migrationTableMigration->mock->shouldReceive('down')->once()->ordered();
$this->pdo->shouldReceive('commit')->once()->ordered();
$this->pdo->shouldReceive('beginTransaction')->once()->ordered();
$animalsTableMigration->mock->shouldReceive('down')->once()->ordered();
$this->pdo->shouldReceive('commit')->once()->ordered();
$result = $this->migrations->down($migrationTableMigration->model, $animalsTableMigration->model);
self::assertTrue($result);
}
/** @test */
public function updatesTheMigrationStatus()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class, 'done');
$migration->mock->shouldReceive('down')->with()->once()->ordered();
$this->mockPreparedStatement('/^update migrations set/i')
->shouldReceive('execute')->withArgs(function (array $values) use ($migration) {
self::assertCount(6, $values);
self::assertSame($migration->model->executed->format('c'), array_shift($values));
self::assertSame(date('c'), array_shift($values));
self::assertSame('reverted', array_shift($values));
self::assertSame('[]', array_shift($values));
self::assertInternalType('double', array_shift($values));
self::assertSame('@breyta/CreateMigrationTable.php', array_shift($values));
return true;
})->once()->andReturn(1)->ordered();
$result = $this->migrations->down($migration->model);
self::assertTrue($result);
}
/** @test */
public function pdoExceptionCausesARollback()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class, 'done');
$migration->mock->shouldReceive('down')->with()->once()->andThrows(\PDOException::class)->ordered();
$this->pdo->shouldReceive('rollback')->once()->ordered();
self::expectException(\PDOException::class);
$this->migrations->down($migration->model);
}
/** @test */
public function addsExecutedStatementsAndExecutionTime()
{
list($migration) = $this->mockMigrations([
'model' => Migration::createInstance([
'file' => 'CreateAnimalsTable.php',
'status' => 'done',
'executed' => date('c', strtotime('-1 hour')),
'statements' => json_encode([Statement::createInstance([
'raw' => 'DROP TABLE IF EXISTS animals',
'teaser' => 'DROP TABLE animals',
'action' => 'drop',
'type' => 'table',
'name' => 'animals',
]), Statement::createInstance([
'raw' => 'CREATE TABLE animals (id MEDIUMINT NOT NULL AUTO_INCREMENT, PRIMARY KEY (id))',
'teaser' => 'CREATE TABLE animals',
'action' => 'create',
'type' => 'table',
'name' => 'animals',
])]),
'executionTime' => 300
]),
'class' => CreateAnimalsTable::class
]);
$this->mockExecuteStatementOn($migration, 'down', $statement = Statement::createInstance([
'raw' => 'DROP TABLE animals',
'teaser' => 'DROP TABLE animals',
'action' => 'drop',
'type' => 'table',
'name' => 'animals',
]));
self::assertCount(3, $migration->model->statements);
self::assertSame($statement, end($migration->model->statements));
}
}
......@@ -2,52 +2,19 @@
namespace Breyta\Test\Migrations;
use Breyta\AdapterInterface;
use Breyta\BasicAdapter;
use Breyta\Migration\CreateMigrationTable;
use Breyta\Migrations;
use Breyta\Model\Migration;
use Breyta\Model\Statement;
use Breyta\Test\Example\CreateAnimalsTable;
use Breyta\Test\TestCase;
use Mockery as m;
class MigrateTest extends TestCase
{
/** @var m\Mock|\PDOStatement */
protected $statement;
/** @var m\Mock|Migrations */
protected $migrations;
/** @var m\Mock */
protected $resolver;
/** @var callable */
protected $executor;
protected function setUp()
{
parent::setUp();
$resolver = $this->resolver = m::spy(function ($class, ...$args) {
return new $class(...$args);
});
$this->migrations = m::mock(Migrations::class, [$this->pdo, __DIR__ . '/../Example', $resolver])
->makePartial();
$resolver->shouldReceive('__invoke')->with(AdapterInterface::class, m::type(\Closure::class))
->andReturnUsing(function ($class, callable $executor) {
$this->executor = m::spy($executor);
return new BasicAdapter($this->executor);
})->byDefault();
$this->mockPreparedStatement('/^insert into migrations/i', true);
$this->mockPreparedStatement('/^delete from migrations/i', true, 0);
}
/** @test */
public function returnsSuccessWhenNoMigrationsNeedToBeExecuted()
{
$this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class, 'done');
$this->mockStatus(Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'done'
]));
$this->migrations->shouldReceive('up')->with()->once()->andReturn(true);
$result = $this->migrations->migrate();
......@@ -57,9 +24,11 @@ class MigrateTest extends TestCase
/** @test */
public function executesNewMigrations()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$migration->shouldReceive('up')->with()->once();
$this->mockStatus($migration = Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'new',
]));
$this->migrations->shouldReceive('up')->with($migration)->once()->andReturn(true);
$result = $this->migrations->migrate();
......@@ -69,77 +38,12 @@ class MigrateTest extends TestCase
/** @test */
public function executesFailedMigrations()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$migration->shouldReceive('up')->with()->once();
$result = $this->migrations->migrate();
self::assertTrue($result);
}
/** @test */
public function executesOnlyMatchingMigrations()
{
list($migrationTableMigration, $animalsTableMigration) = $this->mockMigrations([
$this->mockStatus($migration = Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'class' => CreateMigrationTable::class,
'status' => 'new'
], [
'file' => 'CreateAnimalsTable.php',
'class' => CreateAnimalsTable::class,
'status' => 'new'
]);
$migrationTableMigration->shouldReceive('up')->with()->once();
$animalsTableMigration->shouldNotReceive('up');
$result = $this->migrations->migrate('MigrationTable');
self::assertTrue($result);
}
/** @test */
public function executesMigrationsInSeparateTransactions()
{
list($migrationTableMigration, $animalsTableMigration) = $this->mockMigrations([
'file' => '@breyta/CreateMigrationTable.php',
'class' => CreateMigrationTable::class,
'status' => 'new'
], [
'file' => 'CreateAnimalsTable.php',
'class' => CreateAnimalsTable::class,
'status' => 'new'
]);
$this->pdo->shouldReceive('beginTransaction')->once()->ordered();
$migrationTableMigration->shouldReceive('up')->once()->ordered();
$this->pdo->shouldReceive('commit')->once()->ordered();
$this->pdo->shouldReceive('beginTransaction')->once()->ordered();
$animalsTableMigration->shouldReceive('up')->once()->ordered();
$this->pdo->shouldReceive('commit')->once()->ordered();
$result = $this->migrations->migrate();
self::assertTrue($result);
}
/** @test */
public function savesTheMigrationStatus()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$migration->shouldReceive('up')->with()->once()->ordered();
$this->mockPreparedStatement('/^insert into migrations/i')
->shouldReceive('execute')->withArgs(function (array $values) {
self::assertCount(5, $values);
self::assertSame('@breyta/CreateMigrationTable.php', array_shift($values));
self::assertSame(date('c'), array_shift($values));
self::assertSame('done', array_shift($values));
self::assertSame('[]', array_shift($values));
self::assertInternalType('double', array_shift($values));
return true;
})->once()->andReturn(1)->ordered();
'status' => 'failed',
'executed' => date('c', strtotime('-1 hour')),
]));
$this->migrations->shouldReceive('up')->with($migration)->once()->andReturn(true);
$result = $this->migrations->migrate();
......@@ -147,204 +51,18 @@ class MigrateTest extends TestCase
}
/** @test */
public function removesPreviousStatus()
public function executesRevertedMigrations()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class, 'failed');
$migration->shouldReceive('up')->with()->once()->ordered();
$this->mockPreparedStatement('/^delete from migrations/i')
->shouldReceive('execute')->with(['@breyta/CreateMigrationTable.php'])
->once()->andReturn(1)->ordered();
$this->mockStatus($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 pdoExceptionCausesARollback()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$migration->shouldReceive('up')->with()->once()->andThrows(\PDOException::class)->ordered();
$this->pdo->shouldReceive('rollback')->once()->ordered();
self::expectException(\PDOException::class);
$this->migrations->migrate();
}
/** @test */
public function savesFailedStatus()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$migration->shouldReceive('up')->with()->once()->andThrows(\PDOException::class)->ordered();
$this->mockPreparedStatement('/^insert into migrations/i')
->shouldReceive('execute')->withArgs(function (array $values) {
self::assertCount(5, $values);
self::assertSame('@breyta/CreateMigrationTable.php', array_shift($values));
self::assertSame(date('c'), array_shift($values));
self::assertSame('failed', array_shift($values));
self::assertSame('[]', array_shift($values));
self::assertInternalType('double', array_shift($values));
return true;
})->once()->andReturn(1)->ordered();
self::expectException(\PDOException::class);
$this->migrations->migrate();
}
/** @test */
public function executorRequiresAStatement()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$migration->shouldReceive('up')->with()
->once()->andReturnUsing(function () {
call_user_func($this->executor, 'CREATE TABLE migrations (col INT NOT NULL, PRIMARY KEY (col))');
});
self::expectException(\Error::class);
self::expectExceptionMessage(' must be an instance of Breyta\Model\Statement, string given');
$this->migrations->migrate();
}
/** @test */
public function executorExecutesTheStatement()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$migration->shouldReceive('up')->with()
->once()->andReturnUsing(function () {
call_user_func($this->executor, Statement::createInstance([
'raw' => 'CREATE TABLE migrations (col INT NOT NULL, PRIMARY KEY (col))',
]));
})->ordered();
$this->pdo->shouldReceive('exec')->with('CREATE TABLE migrations (col INT NOT NULL, PRIMARY KEY (col))')
->once()->andReturn(1);
$this->migrations->migrate();
}
/** @test */
public function addsExecutionTimeToStatement()
{
$statement = Statement::createInstance([
'raw' => 'CREATE TABLE migrations (col INT NOT NULL, PRIMARY KEY (col))',
]);
$this->mockExecuteStatement($statement);
self::assertInternalType('double', $statement->executionTime);
}
/** @test */
public function addsThrownExceptionToStatement()
{
$statement = Statement::createInstance([
'raw' => 'CREATE TABLE migrations (col INT NOT NULL, PRIMARY KEY (col))',
]);
try {
$this->mockExecuteStatement($statement, new \PDOException('Failed'));
$this->fail('Expected PDOException');
} catch (\PDOException $exception) {
self::assertSame($exception, $statement->exception);
self::assertInternalType('double', $statement->executionTime);
}
}
/** @test */
public function addsStatementsToMigrationStatus()
{
$statement = Statement::createInstance([
'raw' => 'CREATE TABLE migrations (col INT NOT NULL, PRIMARY KEY (col))',
'teaser' => 'CREATE TABLE migrations',
'action' => 'create',
'type' => 'table',
'name' => 'migrations',
]);
$this->mockPreparedStatement('/^insert into migrations/i')
->shouldReceive('execute')->withArgs(function (array $values) use ($statement) {
self::assertCount(5, $values);
self::assertSame(
json_encode([$statement]),
$values[3]
);
return true;
})->once()->andReturn(1);
$this->mockExecuteStatement($statement);
}
protected function mockStatus(Migration ...$migrations): m\CompositeExpectation
{
$status = (object)[
'migrations' => array_combine(array_map(function (Migration $migration) {
return $migration->file;
}, $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'): m\MockInterface
{
return $this->mockMigrations(['file' => $file, 'class' => $class, 'status' => $status])[0];
}
protected function mockMigrations(...$migrations)
{
$instances = [];
$classes = [];
$migrationStatus = [];
foreach ($migrations as $migration) {
$classes[$migration['file']] = $migration['class'];
$this->resolver->shouldReceive('__invoke')->with($migration['class'], m::type(AdapterInterface::class))
->andReturn($instances[] = m::mock(CreateMigrationTable::class));
$migrationStatus[] = Migration::createInstance([
'file' => $migration['file'],
'status' => $migration['status'],
]);
}
// add the file -> class mapping
$this->setProtectedProperty(
$this->migrations,
'classes',
array_merge(
$this->getProtectedProperty($this->migrations, 'classes') ?? [],
$classes
)
);
$this->mockStatus(...$migrationStatus);
return $instances;
}
protected function mockExecuteStatement(Statement $statement, $result = 1)
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$migration->shouldReceive('up')->with()
->once()->andReturnUsing(function () use ($statement) {
call_user_func($this->executor, $statement);
})->ordered();
$expectation = $this->pdo->shouldReceive('exec')->with($statement->raw)
->once();
if ($result instanceof \Exception) {
$expectation->andThrow($result);
} else {
$expectation->andReturn($result);
}
$this->migrations->migrate();
}
}
<?php
namespace Breyta\Test\Migrations;
use Breyta\Model\Migration;
use Breyta\Test\TestCase;
class RevertTest extends TestCase
{
/** @test */
public function returnsSuccessWhenNoMigrationsNeedToBeReverted()
{
$this->mockStatus(Migration::createInstance([
'file' => 'CreateAnimalsTable.php',
'status' => 'new'
]));
$this->migrations->shouldReceive('down')->with()->once()->andReturn(true);
$result = $this->migrations->revert();
self::assertTrue($result);
}
/** @test */
public function revertsDoneMigrations()
{
$this->mockStatus($migration = Migration::createInstance([
'file' => 'CreateAnimalsTable.php',
'status' => 'done',
]));
$this->migrations->shouldReceive('down')->with($migration)->once()->andReturn(true);
$result = $this->migrations->revert();
self::assertTrue($result);
}
/** @test */
public function doesNotRevertInternalMigrations()
{
$this->mockStatus(Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'done'
]));
$this->migrations->shouldReceive('down')->with()->once()->andReturn(true);
$result = $this->migrations->revert();
self::assertTrue($result);
}
/** @test */
public function doesNotRevertFailedMigrations()
{
$this->mockStatus(Migration::createInstance([
'file' => 'CreateAnimalsTable.php',
'status' => 'failed'
]));
$this->migrations->shouldReceive('down')->with()->once()->andReturn(true);
$result = $this->migrations->revert();
self::assertTrue($result);
}
/** @test */
public function doesNotRevertRevertedMigrations()
{
$this->mockStatus(Migration::createInstance([
'file' => 'CreateAnimalsTable.php',
'status' => 'reverted'
]));
$this->migrations->shouldReceive('down')->with()->once()->andReturn(true);
$result = $this->migrations->revert();
self::assertTrue($result);
}
}
......@@ -50,7 +50,7 @@ class StatusTest extends TestCase
$status = $migrations->getStatus();
self::assertEquals($migration, $status->migrations['@breyta/CreateMigrationTable.php']);
self::assertEquals($migration, array_shift($status->migrations));
}
/** @test */
......
<?php
namespace Breyta\Test\Migrations;
use Breyta\Migration\CreateMigrationTable;
use Breyta\Model\Statement;
use Breyta\Test\Example\CreateAnimalsTable;
use Breyta\Test\TestCase;
class UpTest extends TestCase
{
/** @test */
public function executesMigrationsInSeparateTransactions()
{
list($migrationTableMigration, $animalsTableMigration) = $this->mockMigrations([
'file' => '@breyta/CreateMigrationTable.php',
'class' => CreateMigrationTable::class,
'status' => 'new'
], [
'file' => 'CreateAnimalsTable.php',
'class' => CreateAnimalsTable::class,
'status' => 'new'
]);
$this->pdo->shouldReceive('beginTransaction')->once()->ordered();
$migrationTableMigration->mock->shouldReceive('up')->once()->ordered();
$this->pdo->shouldReceive('commit')->once()->ordered();
$this->pdo->shouldReceive('beginTransaction')->once()->ordered();
$animalsTableMigration->mock->shouldReceive('up')->once()->ordered();
$this->pdo->shouldReceive('commit')->once()->ordered();
$result = $this->migrations->up($migrationTableMigration->model, $animalsTableMigration->model);
self::assertTrue($result);
}
/** @test */
public function savesTheMigrationStatus()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$migration->mock->shouldReceive('up')->with()->once()->ordered();
$this->mockPreparedStatement('/^insert into migrations/i')
->shouldReceive('execute')->withArgs(function (array $values) {
self::assertCount(5, $values);
self::assertSame('@breyta/CreateMigrationTable.php', array_shift($values));
self::assertSame(date('c'), array_shift($values));
self::assertSame('done', array_shift($values));
self::assertSame('[]', array_shift($values));
self::assertInternalType('double', array_shift($values));
return true;
})->once()->andReturn(1)->ordered();
$result = $this->migrations->up($migration->model);
self::assertTrue($result);
}
/** @test */
public function updatesTheMigrationStatus()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class, 'failed');
$migration->mock->shouldReceive('up')->with()->once()->ordered();
$this->mockPreparedStatement('/^update migrations set/i')
->shouldReceive('execute')->withArgs(function (array $values) {
self::assertCount(6, $values);
self::assertSame(date('c'), array_shift($values));
self::assertSame(null, array_shift($values));
self::assertSame('done', array_shift($values));
self::assertSame('[]', array_shift($values));
self::assertInternalType('double', array_shift($values));
self::assertSame('@breyta/CreateMigrationTable.php', array_shift($values));
return true;
})->once()->andReturn(1)->ordered();
$result = $this->migrations->up($migration->model);
self::assertTrue($result);
}
/** @test */
public function pdoExceptionCausesARollback()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$migration->mock->shouldReceive('up')->with()->once()->andThrows(\PDOException::class)->ordered();
$this->pdo->shouldReceive('rollback')->once()->ordered();
self::expectException(\PDOException::class);
$this->migrations->up($migration->model);
}
/** @test */
public function savesFailedStatus()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$migration->mock->shouldReceive('up')->with()->once()->andThrows(\PDOException::class)->ordered();
$this->mockPreparedStatement('/^insert into migrations/i')
->shouldReceive('execute')->withArgs(function (array $values) {
self::assertCount(5, $values);
self::assertSame('@breyta/CreateMigrationTable.php', array_shift($values));
self::assertSame(date('c'), array_shift($values));
self::assertSame('failed', array_shift($values));
self::assertSame('[]', array_shift($values));
self::assertInternalType('double', array_shift($values));
return true;
})->once()->andReturn(1)->ordered();
self::expectException(\PDOException::class);
$this->migrations->up($migration->model);
}
/** @test */
public function executorRequiresAStatement()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$migration->mock->shouldReceive('up')->with()
->once()->andReturnUsing(function () {
call_user_func($this->executor, 'CREATE TABLE migrations (col INT NOT NULL, PRIMARY KEY (col))');
});
self::expectException(\Error::class);
self::expectExceptionMessage(' must be an instance of Breyta\Model\Statement, string given');
$this->migrations->up($migration->model);
}
/** @test */
public function executorExecutesTheStatement()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$migration->mock->shouldReceive('up')->with()
->once()->andReturnUsing(function () {
call_user_func($this->executor, Statement::createInstance([
'raw' => 'CREATE TABLE migrations (col INT NOT NULL, PRIMARY KEY (col))',
]));
})->ordered();
$this->pdo->shouldReceive('exec')->with('CREATE TABLE migrations (col INT NOT NULL, PRIMARY KEY (col))')
->once()->andReturn(1);
$this->migrations->up($migration->model);
}
/** @test */
public function addsExecutionTimeToStatement()
{
$statement = Statement::createInstance([
'raw' => 'CREATE TABLE migrations (col INT NOT NULL, PRIMARY KEY (col))',
]);
$this->mockExecuteStatement($statement);
self::assertInternalType('double', $statement->executionTime);
}
/** @test */
public function addsThrownExceptionToStatement()
{
$statement = Statement::createInstance([
'raw' => 'CREATE TABLE migrations (col INT NOT NULL, PRIMARY KEY (col))',
]);
try {
$this->mockExecuteStatement($statement, new \PDOException('Failed'));
$this->fail('Expected PDOException');
} catch (\PDOException $exception) {
self::assertSame($exception, $statement->exception);
self::assertInternalType('double', $statement->executionTime);
}
}
/** @test */
public function addsStatementsToMigrationStatus()
{
$statement = Statement::createInstance([
'raw' => 'CREATE TABLE migrations (col INT NOT NULL, PRIMARY KEY (col))',
'teaser' => 'CREATE TABLE migrations',
'action' => 'create',
'type' => 'table',
'name' => 'migrations',
]);
$this->mockPreparedStatement('/^insert into migrations/i')
->shouldReceive('execute')->withArgs(function (array $values) use ($statement) {
self::assertCount(5, $values);
self::assertSame(
json_encode([$statement]),
$values[3]
);
return true;
})->once()->andReturn(1);
$this->mockExecuteStatement($statement);
}
}
......@@ -2,6 +2,13 @@
namespace Breyta\Test;
use Breyta\AbstractMigration;
use Breyta\AdapterInterface;
use Breyta\BasicAdapter;
use Breyta\Migration\CreateMigrationTable;
use Breyta\Migrations;
use Breyta\Model\Migration;
use Breyta\Model\Statement;
use Mockery\Adapter\Phpunit\MockeryTestCase;
use Mockery as m;
......@@ -10,17 +17,41 @@ abstract class TestCase extends MockeryTestCase
/** @var m\Mock */
protected $pdo;
/** @var m\Mock|\PDOStatement */
protected $statement;
/** @var m\Mock|Migrations */
protected $migrations;
/** @var m\Mock */
protected $resolver;
/** @var callable */
protected $executor;
protected function setUp()
{
date_default_timezone_set('UTC');
$pdo = $this->pdo = m::mock(\PDO::class);
$pdo->shouldReceive('setAttribute')->with(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION)
->andReturn(true)->byDefault();
$pdo->shouldReceive('setAttribute')->andReturn(true)->byDefault();
$pdo->shouldReceive('beginTransaction')->byDefault();
$pdo->shouldReceive('commit')->byDefault();
$pdo->shouldReceive('rollback')->byDefault();
$pdo->shouldReceive('query')->andReturn(false)->byDefault();
$this->mockPreparedStatement('/^insert into migrations/i', true);
$this->mockPreparedStatement('/^update migrations set/i', true, 0);
$resolver = $this->resolver = m::spy(function ($class, ...$args) {
return new $class(...$args);
});
$this->migrations = m::mock(Migrations::class, [$this->pdo, __DIR__ . '/Example', $resolver])
->makePartial();
$resolver->shouldReceive('__invoke')->with(AdapterInterface::class, m::type(\Closure::class))
->andReturnUsing(function ($class, callable $executor) {
$this->executor = m::spy($executor);
return new BasicAdapter($this->executor);
})->byDefault();
}
protected function mockPreparedStatement(string $pattern, $byDefault = false, $defaultResult = 1)
......@@ -38,6 +69,73 @@ 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];
}
protected function mockMigrations(...$migrationProperties)
{
$migrations = [];
foreach ($migrationProperties as $migration) {
$migrations[] = (object)[
'model' => isset($migration['model']) ? $migration['model'] : Migration::createInstance([
'file' => $migration['file'],
'status' => $migration['status'],
'executed' => $migration['status'] === 'new' ? null : date('c', strtotime('-1 hour')),
]),
'mock' => $mock = m::mock($migration['class']),
];
$this->resolver->shouldReceive('__invoke')->with($migration['class'], m::type(AdapterInterface::class))
->andReturn($mock);
}
return $migrations;
}
protected function mockExecuteStatement(Statement $statement, $result = 1)
{
$this->mockExecuteStatementOn(
$this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class),
'up',
$statement,
$result
);
}
protected function mockExecuteStatementOn(
\stdClass $migration,
string $direction,
Statement $statement,
$result = 1
) {
$migration->mock->shouldReceive($direction)->with()
->once()->andReturnUsing(function () use ($statement) {
call_user_func($this->executor, $statement);
})->ordered();
$expectation = $this->pdo->shouldReceive('exec')->with($statement->raw)
->once();
if ($result instanceof \Exception) {
$expectation->andThrow($result);
} else {
$expectation->andReturn($result);
}
$this->migrations->$direction($migration->model);
}
protected function setProtectedProperty($obj, $propertyName, $value)
{
$reflection = new \ReflectionClass($obj);
......
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