Skip to content

Commit

Permalink
Add support for custom binary paths (#29)
Browse files Browse the repository at this point in the history
* Add support for custom binary paths

Users can now configure the exact path to the mysql and mysqldump binaries for different kind of set ups.

* Don't typehint the execute() method just yet

* Fix style in snipe config

* Add proper snipe test support

These changes now properly reset the application state before each test run, as well as adds a few helpful methods to deal with snipe internals.

* Add test for checking for the migrate:fresh command

* Add test to detect and reflect database changes

* Fix style issues in SnipeMigrationsTest

* Add test for custom binary prefixes

This uses a mockery extension package to overwrite the native “exec” method with a customizable mock.

* Fix style issues in SnipeMigrationsTest

* Add back the snapshots folder

I seem to have deleted this by mistake
  • Loading branch information
spaceemotion authored and drfraker committed Sep 27, 2019
1 parent 2338c63 commit 70dc1e0
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 13 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
16 changes: 16 additions & 0 deletions config/snipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
],

];
2 changes: 1 addition & 1 deletion snapshots/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
.snipe
snipe_snapshot.sql
snipe_snapshot.sql
26 changes: 24 additions & 2 deletions src/Snipe.php
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

/**
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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");
}
}
128 changes: 119 additions & 9 deletions tests/SnipeMigrationsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,159 @@

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 */
public function it_throws_an_error_if_the_application_is_using_in_memory_database()
{
$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;
}
}

0 comments on commit 70dc1e0

Please sign in to comment.