diff --git a/composer.json b/composer.json index 93068b6..2bfc739 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ }, "require-dev": { "phpunit/phpunit": "^7.0", - "orchestra/testbench": "^3.7" + "orchestra/testbench": "^3.7", + "php-mock/php-mock-mockery": "^1.3" }, "autoload": { "psr-4": { diff --git a/config/snipe.php b/config/snipe.php index 9518976..51e9d6e 100644 --- a/config/snipe.php +++ b/config/snipe.php @@ -25,4 +25,20 @@ 'seed-database' => false, 'seed-class' => 'DatabaseSeeder', + /* + |-------------------------------------------------------------------------- + | Command Execution + |-------------------------------------------------------------------------- + | Many systems have the mysql binaries already installed (e.g. Homestead). + | In case the binaries lie at a different path or have a special prefix, + | as seen in docker-based setups, they can be configured here. + | + | e.g. 'mysql' => 'docker-compose test_db mysql' + */ + + 'binaries' => [ + 'mysql' => 'mysql', + 'mysqldump' => 'mysqldump', + ], + ]; diff --git a/snapshots/.gitignore b/snapshots/.gitignore index 326faab..90a2cff 100644 --- a/snapshots/.gitignore +++ b/snapshots/.gitignore @@ -1,2 +1,2 @@ .snipe -snipe_snapshot.sql \ No newline at end of file +snipe_snapshot.sql diff --git a/src/Snipe.php b/src/Snipe.php index 8c7f47d..08f56c9 100644 --- a/src/Snipe.php +++ b/src/Snipe.php @@ -74,7 +74,7 @@ protected function newSnapshot() $storageLocation = config('snipe.snapshot-location'); // Store a snapshot of the db after migrations run. - exec("mysqldump -h {$this->getDbHost()} -u {$this->getDbUsername()} --password={$this->getDbPassword()} {$this->getDbName()} > {$storageLocation} 2>/dev/null"); + $this->execute('mysqldump', "-h {$this->getDbHost()} -u {$this->getDbUsername()} --password={$this->getDbPassword()} {$this->getDbName()} > {$storageLocation} 2>/dev/null"); } /** @@ -134,7 +134,7 @@ protected function importDatabase() if (! SnipeDatabaseState::$importedDatabase) { $dumpfile = config('snipe.snapshot-location'); - exec("mysql -h {$this->getDbHost()} -u {$this->getDbUsername()} --password={$this->getDbPassword()} {$this->getDbName()} < {$dumpfile} 2>/dev/null"); + $this->execute('mysql', "-h {$this->getDbHost()} -u {$this->getDbUsername()} --password={$this->getDbPassword()} {$this->getDbName()} < {$dumpfile} 2>/dev/null"); SnipeDatabaseState::$importedDatabase = true; } @@ -197,4 +197,26 @@ protected function getDbName() return config("database.connections.{$connection}.database"); } + + /** + * Returns the path to the given binary executable. + * + * @param string $binary + * @return string + */ + protected function getBinaryPath($binary) + { + return config("snipe.binaries.$binary", $binary); + } + + /** + * Executes the given command. + * + * @param string $binary + * @param string $command + */ + protected function execute($binary, $command) + { + exec("{$this->getBinaryPath($binary)} $command"); + } } diff --git a/tests/SnipeMigrationsTest.php b/tests/SnipeMigrationsTest.php index e84be9b..2dabbad 100644 --- a/tests/SnipeMigrationsTest.php +++ b/tests/SnipeMigrationsTest.php @@ -2,20 +2,57 @@ namespace Drfraker\SnipeMigrations\Tests; +use phpmock\mockery\PHPMockery; use Drfraker\SnipeMigrations\Snipe; +use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Artisan; +use Drfraker\SnipeMigrations\SnipeDatabaseState; class SnipeMigrationsTest extends TestCase { + /** @var Snipe */ protected $snipe; + /** + * The absolute path to the "snapshots" folder. + * @var string + */ + protected $snipeFolder; + + /** + * The full path to the snipe_snapshot.sql file. + * @var string + */ + protected $snapshotFile; + + /** + * The full path to the .snip file. + * @var string + */ + protected $snipeFile; + public function setUp() :void { parent::setUp(); + // This folder will reside in the orchestra sandbox environment + // placed at vendor/orchestra/testbench-core/laravel + $this->snipeFolder = base_path('vendor/drfraker/snipe-migrations/snapshots'); + + $this->snapshotFile = config('snipe.snapshot-location'); + $this->snipeFile = config('snipe.snipefile-location'); + + // Reset state before each run $this->clearSnapshotDir(); + $this->clearMigrationsDir(); + $this->resetDatabaseState(); $this->snipe = new Snipe(); + + // Add support for native method mocking. We pre-define the exec method here. + // If we would just call the mock method in our tests where we need it, PHP + // would have already cached the call to the native method instead. + PHPMockery::define('Drfraker\SnipeMigrations', 'exec'); } /** @test */ @@ -23,28 +60,101 @@ public function it_throws_an_error_if_the_application_is_using_in_memory_databas { $this->mimicInMemoryDatabase(); - Artisan::ShouldReceive('call')->never(); + Artisan::shouldReceive('call')->never(); + + $this->snipe->importSnapshot(); + } + + /** @test */ + public function it_calls_migration_commands_for_mysql_databases() + { + Artisan::shouldReceive('call')->with('migrate:fresh'); + + $this->snipe->importSnapshot(); + } + + /** @test */ + public function it_detects_file_changes_in_the_migration_folder() + { + Artisan::shouldReceive('call'); + + // The first time we run snipe, we have no migrations + $this->snipe->importSnapshot(); + + $this->assertFileExists($this->snipeFile); + $this->assertEquals(0, file_get_contents($this->snipeFile)); + + $this->copyDefaultMigrations(); + + // Let's do a re-run + $this->resetDatabaseState(); + $this->snipe->importSnapshot(); + + // This time the changes should have been picked up + $this->assertGreaterThan(0, file_get_contents($this->snipeFile)); + } + + /** @test */ + public function it_allows_a_custom_prefix_for_executables() + { + Artisan::shouldReceive('call')->withAnyArgs(); + + // Define the custom binary path we want to use + static $customBinary = 'docker-compose exec db mysql'; + config()->set(['snipe.binaries.mysqldump' => $customBinary]); + + PHPMockery::mock('Drfraker\SnipeMigrations', 'exec') + ->withArgs(static function ($args) use ($customBinary) { + return strpos($args, $customBinary) === 0; + })->once(); $this->snipe->importSnapshot(); } protected function mimicInMemoryDatabase(): void { - config()->set('database.default', 'sqlite'); - config()->set('database.connections.sqlite.database', ':memory:'); + config()->set([ + 'database.default' => 'sqlite', + 'database.connections.sqlite.database' => ':memory:', + ]); } protected function clearSnapshotDir() { - $snipefile = '../snapshots/.snipe'; - $snapshot = '../snapshots/snipe_snapshot.sql'; + if (! is_dir($this->snipeFolder)) { + mkdir($this->snipeFolder, 0777, true); + + return; + } - if (file_exists($snipefile)) { - unlink($snipefile); + // Prepare sandbox for subsequent runs + if (file_exists($this->snipeFile)) { + $this->assertTrue(unlink($this->snipeFile)); } - if (file_exists($snapshot)) { - unlink($snapshot); + if (file_exists($this->snapshotFile)) { + $this->assertTrue(unlink($this->snapshotFile)); } } + + protected function clearMigrationsDir() + { + foreach (File::allFiles(database_path('migrations')) as $file) { + if ($file->getFilename() !== '.gitkeep') { + unlink($file->getRealPath()); + } + } + } + + protected function copyDefaultMigrations(): void + { + foreach (File::allFiles(base_path('migrations')) as $file) { + copy($file->getRealPath(), database_path("migrations/{$file->getFilename()}")); + } + } + + protected function resetDatabaseState(): void + { + SnipeDatabaseState::$checkedForDatabaseFileChanges = false; + } }