diff --git a/composer.json b/composer.json index 581d0ead203b..df3e46d1e1ae 100644 --- a/composer.json +++ b/composer.json @@ -129,6 +129,7 @@ "ext-posix": "Required to use all features of the queue worker.", "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).", "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.155).", + "brianium/paratest": "Required to run tests in parallel (^6.0).", "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6|^3.0).", "filp/whoops": "Required for friendly error pages in development (^2.8).", "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 80611bfc92ed..04f96e43308a 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -94,6 +94,28 @@ public static function morphUsingUuids() return static::defaultMorphKeyType('uuid'); } + /** + * Create a database in the schema. + * + * @param string $name + * @return bool + */ + public function createDatabase($name) + { + throw new LogicException('This database driver does not support creating databases.'); + } + + /** + * Drop a database from the schema if the database exists. + * + * @param string $name + * @return bool + */ + public function dropDatabaseIfExists($name) + { + throw new LogicException('This database driver does not support dropping databases.'); + } + /** * Determine if the given table exists. * diff --git a/src/Illuminate/Database/Schema/Grammars/Grammar.php b/src/Illuminate/Database/Schema/Grammars/Grammar.php index b60dfe817b62..18071b2fbb12 100755 --- a/src/Illuminate/Database/Schema/Grammars/Grammar.php +++ b/src/Illuminate/Database/Schema/Grammars/Grammar.php @@ -9,6 +9,7 @@ use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Fluent; +use LogicException; use RuntimeException; abstract class Grammar extends BaseGrammar @@ -27,6 +28,29 @@ abstract class Grammar extends BaseGrammar */ protected $fluentCommands = []; + /** + * Compile a create database command. + * + * @param string $name + * @param \Illuminate\Database\Connection $connection + * @return string + */ + public function compileCreateDatabase($name, $connection) + { + throw new LogicException('This database driver does not support creating databases.'); + } + + /** + * Compile a drop database if exists command. + * + * @param string $name + * @return string + */ + public function compileDropDatabaseIfExists($name) + { + throw new LogicException('This database driver does not support dropping databases.'); + } + /** * Compile a rename column command. * diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index c2952e47926c..ecaf96e2a3a1 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -26,6 +26,37 @@ class MySqlGrammar extends Grammar */ protected $serials = ['bigInteger', 'integer', 'mediumInteger', 'smallInteger', 'tinyInteger']; + /** + * Compile a create database command. + * + * @param string $name + * @param \Illuminate\Database\Connection $connection + * @return string + */ + public function compileCreateDatabase($name, $connection) + { + return sprintf( + 'create database %s default character set %s default collate %s', + $this->wrapValue($name), + $this->wrapValue($connection->getConfig('charset')), + $this->wrapValue($connection->getConfig('collation')), + ); + } + + /** + * Compile a drop database if exists command. + * + * @param string $name + * @return string + */ + public function compileDropDatabaseIfExists($name) + { + return sprintf( + 'drop database if exists %s', + $this->wrapValue($name) + ); + } + /** * Compile the query to determine the list of tables. * diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 204beecea3b2..16737493f9e1 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -35,6 +35,36 @@ class PostgresGrammar extends Grammar */ protected $fluentCommands = ['Comment']; + /** + * Compile a create database command. + * + * @param string $name + * @param \Illuminate\Database\Connection $connection + * @return string + */ + public function compileCreateDatabase($name, $connection) + { + return sprintf( + 'create database %s encoding %s', + $this->wrapValue($name), + $this->wrapValue($connection->getConfig('charset')), + ); + } + + /** + * Compile a drop database if exists command. + * + * @param string $name + * @return string + */ + public function compileDropDatabaseIfExists($name) + { + return sprintf( + 'drop database if exists %s', + $this->wrapValue($name) + ); + } + /** * Compile the query to determine if a table exists. * diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index 79f3f0f5e3ad..c3fc442e2368 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -28,6 +28,35 @@ class SqlServerGrammar extends Grammar */ protected $serials = ['tinyInteger', 'smallInteger', 'mediumInteger', 'integer', 'bigInteger']; + /** + * Compile a create database command. + * + * @param string $name + * @param \Illuminate\Database\Connection $connection + * @return string + */ + public function compileCreateDatabase($name, $connection) + { + return sprintf( + 'create database %s', + $this->wrapValue($name), + ); + } + + /** + * Compile a drop database if exists command. + * + * @param string $name + * @return string + */ + public function compileDropDatabaseIfExists($name) + { + return sprintf( + 'drop database if exists %s', + $this->wrapValue($name) + ); + } + /** * Compile the query to determine if a table exists. * diff --git a/src/Illuminate/Database/Schema/MySqlBuilder.php b/src/Illuminate/Database/Schema/MySqlBuilder.php index f07946c85e23..b7cff5568d1b 100755 --- a/src/Illuminate/Database/Schema/MySqlBuilder.php +++ b/src/Illuminate/Database/Schema/MySqlBuilder.php @@ -4,6 +4,32 @@ class MySqlBuilder extends Builder { + /** + * Create a database in the schema. + * + * @param string $name + * @return bool + */ + public function createDatabase($name) + { + return $this->connection->statement( + $this->grammar->compileCreateDatabase($name, $this->connection) + ); + } + + /** + * Drop a database from the schema if the database exists. + * + * @param string $name + * @return bool + */ + public function dropDatabaseIfExists($name) + { + return $this->connection->statement( + $this->grammar->compileDropDatabaseIfExists($name) + ); + } + /** * Determine if the given table exists. * diff --git a/src/Illuminate/Database/Schema/PostgresBuilder.php b/src/Illuminate/Database/Schema/PostgresBuilder.php index 76673a719a41..82702a802691 100755 --- a/src/Illuminate/Database/Schema/PostgresBuilder.php +++ b/src/Illuminate/Database/Schema/PostgresBuilder.php @@ -4,6 +4,32 @@ class PostgresBuilder extends Builder { + /** + * Create a database in the schema. + * + * @param string $name + * @return bool + */ + public function createDatabase($name) + { + return $this->connection->statement( + $this->grammar->compileCreateDatabase($name, $this->connection) + ); + } + + /** + * Drop a database from the schema if the database exists. + * + * @param string $name + * @return bool + */ + public function dropDatabaseIfExists($name) + { + return $this->connection->statement( + $this->grammar->compileDropDatabaseIfExists($name) + ); + } + /** * Determine if the given table exists. * diff --git a/src/Illuminate/Database/Schema/SQLiteBuilder.php b/src/Illuminate/Database/Schema/SQLiteBuilder.php index 78b6b9c78d2e..6a1dbae23ec6 100644 --- a/src/Illuminate/Database/Schema/SQLiteBuilder.php +++ b/src/Illuminate/Database/Schema/SQLiteBuilder.php @@ -2,8 +2,34 @@ namespace Illuminate\Database\Schema; +use Illuminate\Support\Facades\File; + class SQLiteBuilder extends Builder { + /** + * Create a database in the schema. + * + * @param string $name + * @return bool + */ + public function createDatabase($name) + { + return File::put($name, '') !== false; + } + + /** + * Drop a database from the schema if the database exists. + * + * @param string $name + * @return bool + */ + public function dropDatabaseIfExists($name) + { + return File::exists($name) + ? File::delete($name) + : true; + } + /** * Drop all tables from the database. * diff --git a/src/Illuminate/Database/Schema/SqlServerBuilder.php b/src/Illuminate/Database/Schema/SqlServerBuilder.php index 0b3e47bec9a6..223abd44ed1c 100644 --- a/src/Illuminate/Database/Schema/SqlServerBuilder.php +++ b/src/Illuminate/Database/Schema/SqlServerBuilder.php @@ -4,6 +4,32 @@ class SqlServerBuilder extends Builder { + /** + * Create a database in the schema. + * + * @param string $name + * @return bool + */ + public function createDatabase($name) + { + return $this->connection->statement( + $this->grammar->compileCreateDatabase($name, $this->connection) + ); + } + + /** + * Drop a database from the schema if the database exists. + * + * @param string $name + * @return bool + */ + public function dropDatabaseIfExists($name) + { + return $this->connection->statement( + $this->grammar->compileDropDatabaseIfExists($name) + ); + } + /** * Drop all tables from the database. * diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index 2f39afd43d36..f5ffb33658f5 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -5,6 +5,7 @@ use Illuminate\Http\Request; use Illuminate\Support\AggregateServiceProvider; use Illuminate\Support\Facades\URL; +use Illuminate\Testing\ParallelTestingServiceProvider; use Illuminate\Validation\ValidationException; class FoundationServiceProvider extends AggregateServiceProvider @@ -16,6 +17,7 @@ class FoundationServiceProvider extends AggregateServiceProvider */ protected $providers = [ FormRequestServiceProvider::class, + ParallelTestingServiceProvider::class, ]; /** diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index b32202517ceb..6bbc653102da 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -7,6 +7,7 @@ use Illuminate\Console\Application as Artisan; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Facade; +use Illuminate\Support\Facades\ParallelTesting; use Illuminate\Support\Str; use Mockery; use Mockery\Exception\InvalidCountException; @@ -81,6 +82,8 @@ protected function setUp(): void if (! $this->app) { $this->refreshApplication(); + + ParallelTesting::callSetUpTestCaseCallbacks($this); } $this->setUpTraits(); @@ -152,6 +155,8 @@ protected function tearDown(): void if ($this->app) { $this->callBeforeApplicationDestroyedCallbacks(); + ParallelTesting::callTearDownTestCaseCallbacks($this); + $this->app->flush(); $this->app = null; diff --git a/src/Illuminate/Support/Facades/ParallelTesting.php b/src/Illuminate/Support/Facades/ParallelTesting.php new file mode 100644 index 000000000000..641f9fe90eae --- /dev/null +++ b/src/Illuminate/Support/Facades/ParallelTesting.php @@ -0,0 +1,25 @@ +get('filesystems.default'); - (new Filesystem)->cleanDirectory( - $root = storage_path('framework/testing/disks/'.$disk) - ); + $root = storage_path('framework/testing/disks/'.$disk); + + if ($token = ParallelTesting::token()) { + $root = "{$root}_test_{$token}"; + } + + (new Filesystem)->cleanDirectory($root); static::set($disk, $fake = static::createLocalDriver(array_merge($config, [ 'root' => $root, diff --git a/src/Illuminate/Testing/Concerns/TestDatabases.php b/src/Illuminate/Testing/Concerns/TestDatabases.php new file mode 100644 index 000000000000..5da0b81bd73e --- /dev/null +++ b/src/Illuminate/Testing/Concerns/TestDatabases.php @@ -0,0 +1,162 @@ +whenNotUsingInMemoryDatabase(function ($database) { + if (ParallelTesting::option('recreate_databases')) { + Schema::dropDatabaseIfExists( + $this->testDatabase($database) + ); + } + }); + }); + + ParallelTesting::setUpTestCase(function ($testCase) { + $uses = array_flip(class_uses_recursive(get_class($testCase))); + + $databaseTraits = [ + Testing\DatabaseMigrations::class, + Testing\DatabaseTransactions::class, + Testing\RefreshDatabase::class, + ]; + + if (Arr::hasAny($uses, $databaseTraits)) { + $this->whenNotUsingInMemoryDatabase(function ($database) use ($uses) { + $testDatabase = $this->ensureTestDatabaseExists($database); + + $this->switchToDatabase($testDatabase); + + if (isset($uses[Testing\DatabaseTransactions::class])) { + $this->ensureSchemaIsUpToDate(); + } + }); + } + }); + } + + /** + * Ensure a test database exists and returns its name. + * + * @param string $database + * + * @return string + */ + protected function ensureTestDatabaseExists($database) + { + return tap($this->testDatabase($database), function ($testDatabase) use ($database) { + try { + $this->usingDatabase($testDatabase, function () { + Schema::hasTable('dummy'); + }); + } catch (QueryException $e) { + $this->usingDatabase($database, function () use ($testDatabase) { + Schema::dropDatabaseIfExists($testDatabase); + Schema::createDatabase($testDatabase); + }); + } + }); + } + + /** + * Ensure the current database test schema is up to date. + * + * @return void + */ + protected function ensureSchemaIsUpToDate() + { + if (! static::$schemaIsUpToDate) { + Artisan::call('migrate'); + + static::$schemaIsUpToDate = true; + } + } + + /** + * Runs the given callable using the given database. + * + * @param string $database + * @param callable $database + * @return void + */ + protected function usingDatabase($database, $callable) + { + $original = DB::getConfig('database'); + + try { + $this->switchToDatabase($database); + $callable(); + } finally { + $this->switchToDatabase($original); + } + } + + /** + * Apply the given callback when tests are not using in memory database. + * + * @param callable $callback + * @return void + */ + protected function whenNotUsingInMemoryDatabase($callback) + { + $database = DB::getConfig('database'); + + if ($database != ':memory:') { + $callback($database); + } + } + + /** + * Switch to the given database. + * + * @param string $database + * @return void + */ + protected function switchToDatabase($database) + { + DB::purge(); + + $default = config('database.default'); + + config()->set( + "database.connections.{$default}.database", + $database, + ); + } + + /** + * Returns the test database name. + * + * @return string + */ + protected function testDatabase($database) + { + $token = ParallelTesting::token(); + + return "{$database}_test_{$token}"; + } +} diff --git a/src/Illuminate/Testing/ParallelConsoleOutput.php b/src/Illuminate/Testing/ParallelConsoleOutput.php new file mode 100644 index 000000000000..7444be3b92d7 --- /dev/null +++ b/src/Illuminate/Testing/ParallelConsoleOutput.php @@ -0,0 +1,60 @@ +getVerbosity(), + $output->isDecorated(), + $output->getFormatter(), + ); + + $this->output = $output; + } + + /** + * Writes a message to the output. + * + * @param string|iterable $messages + * @param bool $newline + * @param int $options + * @return void + */ + public function write($messages, bool $newline = false, int $options = 0) + { + $messages = collect($messages)->filter(function ($message) { + return ! Str::contains($message, $this->ignore); + }); + + $this->output->write($messages->toArray(), $newline, $options); + } +} diff --git a/src/Illuminate/Testing/ParallelRunner.php b/src/Illuminate/Testing/ParallelRunner.php new file mode 100644 index 000000000000..e26528703bbe --- /dev/null +++ b/src/Illuminate/Testing/ParallelRunner.php @@ -0,0 +1,140 @@ +options = $options; + + if ($output instanceof ConsoleOutput) { + $output = new ParallelConsoleOutput($output); + } + + $this->runner = new WrapperRunner($options, $output); + } + + /** + * Set the application resolver callback. + * + * @param \Closure|null $resolver + * @return void + */ + public static function resolveApplicationUsing($resolver) + { + static::$applicationResolver = $resolver; + } + + /** + * Runs the test suite. + * + * @return void + */ + public function run(): void + { + (new PhpHandler)->handle($this->options->configuration()->php()); + + $this->forEachProcess(function () { + ParallelTesting::callSetUpProcessCallbacks(); + }); + + try { + $this->runner->run(); + } finally { + $this->forEachProcess(function () { + ParallelTesting::callTearDownProcessCallbacks(); + }); + } + } + + /** + * Returns the highest exit code encountered throughout the course of test execution. + * + * @return int + */ + public function getExitCode(): int + { + return $this->runner->getExitCode(); + } + + /** + * Apply the given callback for each process. + * + * @param callable $callback + * @return void + */ + protected function forEachProcess($callback) + { + collect(range(1, $this->options->processes()))->each(function ($token) use ($callback) { + tap($this->createApplication(), function ($app) use ($callback, $token) { + ParallelTesting::resolveTokenUsing(function () use ($token) { + return $token; + }); + + $callback($app); + })->flush(); + }); + } + + /** + * Creates the application. + * + * @return \Illuminate\Contracts\Foundation\Application + */ + protected function createApplication() + { + $applicationResolver = static::$applicationResolver ?: function () { + $applicationCreator = new class { + use \Tests\CreatesApplication; + }; + + return $applicationCreator->createApplication(); + }; + + return call_user_func($applicationResolver); + } +} diff --git a/src/Illuminate/Testing/ParallelTesting.php b/src/Illuminate/Testing/ParallelTesting.php new file mode 100644 index 000000000000..ba67e16c5c74 --- /dev/null +++ b/src/Illuminate/Testing/ParallelTesting.php @@ -0,0 +1,255 @@ +container = $container; + } + + /** + * Set a callback that should be used when resolving options. + * + * @param \Closure|null $callback + * @return void + */ + public function resolveOptionsUsing($resolver) + { + $this->optionsResolver = $resolver; + } + + /** + * Set a callback that should be used when resolving the unique process token. + * + * @param \Closure|null $callback + * @return void + */ + public function resolveTokenUsing($resolver) + { + $this->tokenResolver = $resolver; + } + + /** + * Register a "setUp" process callback. + * + * @param callable $callback + * @return void + */ + public function setUpProcess($callback) + { + $this->setUpProcessCallbacks[] = $callback; + } + + /** + * Register a "setUp" test case callback. + * + * @param callable $callback + * @return void + */ + public function setUpTestCase($callback) + { + $this->setUpTestCaseCallbacks[] = $callback; + } + + /** + * Register a "tearDown" process callback. + * + * @param callable $callback + * @return void + */ + public function tearDownProcess($callback) + { + $this->tearDownProcessCallbacks[] = $callback; + } + + /** + * Register a "tearDown" test case callback. + * + * @param callable $callback + * @return void + */ + public function tearDownTestCase($callback) + { + $this->tearDownTestCaseCallbacks[] = $callback; + } + + /** + * Call all of the "setUp" process callbacks. + * + * @return void + */ + public function callSetUpProcessCallbacks() + { + $this->whenRunningInParallel(function () { + foreach ($this->setUpProcessCallbacks as $callback) { + $this->container->call($callback, [ + 'token' => $this->token(), + ]); + } + }); + } + + /** + * Call all of the "setUp" test case callbacks. + * + * @param \Illuminate\Foundation\Testing\TestCase $testCase + * @return void + */ + public function callSetUpTestCaseCallbacks($testCase) + { + $this->whenRunningInParallel(function () use ($testCase) { + foreach ($this->setUpTestCaseCallbacks as $callback) { + $this->container->call($callback, [ + 'testCase' => $testCase, + 'token' => $this->token(), + ]); + } + }); + } + + /** + * Call all of the "tearDown" process callbacks. + * + * @return void + */ + public function callTearDownProcessCallbacks() + { + $this->whenRunningInParallel(function () { + foreach ($this->tearDownProcessCallbacks as $callback) { + $this->container->call($callback, [ + 'token' => $this->token(), + ]); + } + }); + } + + /** + * Call all of the "tearDown" test case callbacks. + * + * @param \Illuminate\Foundation\Testing\TestCase $testCase + * @return void + */ + public function callTearDownTestCaseCallbacks($testCase) + { + $this->whenRunningInParallel(function () use ($testCase) { + foreach ($this->tearDownTestCaseCallbacks as $callback) { + $this->container->call($callback, [ + 'testCase' => $testCase, + 'token' => $this->token(), + ]); + } + }); + } + + /** + * Get an parallel testing option. + * + * @param string $option + * @return mixed + */ + public function option($option) + { + $optionsResolver = $this->optionsResolver ?: function ($option) { + $option = 'LARAVEL_PARALLEL_TESTING_'.Str::upper($option); + + return $_SERVER[$option] ?? false; + }; + + return call_user_func($optionsResolver, $option); + } + + /** + * Gets an unique test token. + * + * @return int|false + */ + public function token() + { + return $token = $this->tokenResolver + ? call_user_func($this->tokenResolver) + : ($_SERVER['TEST_TOKEN'] ?? false); + } + + /** + * Apply the callback if tests are running in parallel. + * + * @param callable $callback + * @return void + */ + protected function whenRunningInParallel($callback) + { + if ($this->inParallel()) { + $callback(); + } + } + + /** + * Indicates if the current tests are been run in parallel. + * + * @return bool + */ + protected function inParallel() + { + return ! empty($_SERVER['LARAVEL_PARALLEL_TESTING']) && $this->token(); + } +} diff --git a/src/Illuminate/Testing/ParallelTestingServiceProvider.php b/src/Illuminate/Testing/ParallelTestingServiceProvider.php new file mode 100644 index 000000000000..20b900d2e58e --- /dev/null +++ b/src/Illuminate/Testing/ParallelTestingServiceProvider.php @@ -0,0 +1,38 @@ +app->runningInConsole()) { + $this->bootTestDatabase(); + } + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + if ($this->app->runningInConsole()) { + $this->app->singleton(ParallelTesting::class, function () { + return new ParallelTesting($this->app); + }); + } + } +} diff --git a/src/Illuminate/Testing/composer.json b/src/Illuminate/Testing/composer.json index 9b15533bd7c8..2dfe3b64eac3 100644 --- a/src/Illuminate/Testing/composer.json +++ b/src/Illuminate/Testing/composer.json @@ -31,6 +31,7 @@ } }, "suggest": { + "brianium/paratest": "Required to run tests in parallel (^6.0).", "illuminate/console": "Required to assert console commands (^8.0).", "illuminate/database": "Required to assert databases (^8.0).", "illuminate/http": "Required to assert responses (^8.0).", diff --git a/tests/Database/DatabaseAbstractSchemaGrammarTest.php b/tests/Database/DatabaseAbstractSchemaGrammarTest.php new file mode 100755 index 000000000000..04e23eb264be --- /dev/null +++ b/tests/Database/DatabaseAbstractSchemaGrammarTest.php @@ -0,0 +1,37 @@ +expectException(LogicException::class); + $this->expectExceptionMessage('This database driver does not support creating databases.'); + + $grammar->compileCreateDatabase('foo', m::mock(Connection::class)); + } + + public function testDropDatabaseIfExists() + { + $grammar = new class extends Grammar {}; + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('This database driver does not support dropping databases.'); + + $grammar->compileDropDatabaseIfExists('foo'); + } +} diff --git a/tests/Database/DatabaseMySqlBuilderTest.php b/tests/Database/DatabaseMySqlBuilderTest.php new file mode 100644 index 000000000000..dd36209eb40e --- /dev/null +++ b/tests/Database/DatabaseMySqlBuilderTest.php @@ -0,0 +1,48 @@ +shouldReceive('getConfig')->once()->with('charset')->andReturn('utf8mb4'); + $connection->shouldReceive('getConfig')->once()->with('collation')->andReturn('utf8mb4_unicode_ci'); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('statement')->once()->with( + 'create database `my_temporary_database` default character set `utf8mb4` default collate `utf8mb4_unicode_ci`' + )->andReturn(true); + + $builder = new MySqlBuilder($connection); + $builder->createDatabase('my_temporary_database'); + } + + public function testDropDatabaseIfExists() + { + $grammar = new MySqlGrammar(); + + $connection = m::mock(Connection::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('statement')->once()->with( + 'drop database if exists `my_database_a`' + )->andReturn(true); + + $builder = new MySqlBuilder($connection); + + $builder->dropDatabaseIfExists('my_database_a'); + } +} diff --git a/tests/Database/DatabaseMySqlSchemaGrammarTest.php b/tests/Database/DatabaseMySqlSchemaGrammarTest.php index c89af64847ad..d35d8ab91792 100755 --- a/tests/Database/DatabaseMySqlSchemaGrammarTest.php +++ b/tests/Database/DatabaseMySqlSchemaGrammarTest.php @@ -1155,6 +1155,48 @@ public function testGrammarsAreMacroable() $this->assertTrue($c); } + public function testCreateDatabase() + { + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->once()->once()->with('charset')->andReturn('utf8mb4_foo'); + $connection->shouldReceive('getConfig')->once()->once()->with('collation')->andReturn('utf8mb4_unicode_ci_foo'); + + $statement = $this->getGrammar()->compileCreateDatabase('my_database_a', $connection); + + $this->assertSame( + 'create database `my_database_a` default character set `utf8mb4_foo` default collate `utf8mb4_unicode_ci_foo`', + $statement + ); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->once()->once()->with('charset')->andReturn('utf8mb4_bar'); + $connection->shouldReceive('getConfig')->once()->once()->with('collation')->andReturn('utf8mb4_unicode_ci_bar'); + + $statement = $this->getGrammar()->compileCreateDatabase('my_database_b', $connection); + + $this->assertSame( + 'create database `my_database_b` default character set `utf8mb4_bar` default collate `utf8mb4_unicode_ci_bar`', + $statement + ); + } + + public function testDropDatabaseIfExists() + { + $statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_a'); + + $this->assertSame( + 'drop database if exists `my_database_a`', + $statement + ); + + $statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_b'); + + $this->assertSame( + 'drop database if exists `my_database_b`', + $statement + ); + } + protected function getConnection() { return m::mock(Connection::class); diff --git a/tests/Database/DatabasePostgresBuilderTest.php b/tests/Database/DatabasePostgresBuilderTest.php new file mode 100644 index 000000000000..490f66cb4ad8 --- /dev/null +++ b/tests/Database/DatabasePostgresBuilderTest.php @@ -0,0 +1,52 @@ +shouldReceive('getConfig')->once()->with('charset')->andReturn('utf8'); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('statement')->once()->with( + 'create database "my_temporary_database" encoding "utf8"' + )->andReturn(true); + + $builder = $this->getBuilder($connection); + $builder->createDatabase('my_temporary_database'); + } + + public function testDropDatabaseIfExists() + { + $grammar = new PostgresGrammar(); + + $connection = m::mock(Connection::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('statement')->once()->with( + 'drop database if exists "my_database_a"' + )->andReturn(true); + + $builder = $this->getBuilder($connection); + + $builder->dropDatabaseIfExists('my_database_a'); + } + + protected function getBuilder($connection) + { + return new PostgresBuilder($connection); + } +} diff --git a/tests/Database/DatabasePostgresSchemaGrammarTest.php b/tests/Database/DatabasePostgresSchemaGrammarTest.php index 052197fd08c4..3a7f80ae3865 100755 --- a/tests/Database/DatabasePostgresSchemaGrammarTest.php +++ b/tests/Database/DatabasePostgresSchemaGrammarTest.php @@ -977,6 +977,44 @@ public function testAddingMultiPolygon() $this->assertSame('alter table "geo" add column "coordinates" geography(multipolygon, 4326) not null', $statements[0]); } + public function testCreateDatabase() + { + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->once()->once()->with('charset')->andReturn('utf8_foo'); + $statement = $this->getGrammar()->compileCreateDatabase('my_database_a', $connection); + + $this->assertSame( + 'create database "my_database_a" encoding "utf8_foo"', + $statement + ); + + $connection = $this->getConnection(); + $connection->shouldReceive('getConfig')->once()->once()->with('charset')->andReturn('utf8_bar'); + $statement = $this->getGrammar()->compileCreateDatabase('my_database_b', $connection); + + $this->assertSame( + 'create database "my_database_b" encoding "utf8_bar"', + $statement + ); + } + + public function testDropDatabaseIfExists() + { + $statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_a'); + + $this->assertSame( + 'drop database if exists "my_database_a"', + $statement + ); + + $statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_b'); + + $this->assertSame( + 'drop database if exists "my_database_b"', + $statement + ); + } + public function testDropAllTablesEscapesTableNames() { $statement = $this->getGrammar()->compileDropAllTables(['alpha', 'beta', 'gamma']); diff --git a/tests/Database/DatabaseSQLiteBuilderTest.php b/tests/Database/DatabaseSQLiteBuilderTest.php new file mode 100644 index 000000000000..b2587ea67d82 --- /dev/null +++ b/tests/Database/DatabaseSQLiteBuilderTest.php @@ -0,0 +1,91 @@ +singleton('files', Filesystem::class); + + Facade::setFacadeApplication($app); + } + + protected function tearDown(): void + { + m::close(); + + Container::setInstance(null); + Facade::setFacadeApplication(null); + } + + public function testCreateDatabase() + { + $connection = m::mock(Connection::class); + $connection->shouldReceive('getSchemaGrammar')->once(); + + $builder = new SQLiteBuilder($connection); + + File::shouldReceive('put') + ->once() + ->with('my_temporary_database_a', '') + ->andReturn(20); // bytes + + $this->assertTrue($builder->createDatabase('my_temporary_database_a')); + + File::shouldReceive('put') + ->once() + ->with('my_temporary_database_b', '') + ->andReturn(false); + + $this->assertFalse($builder->createDatabase('my_temporary_database_b')); + } + + public function testDropDatabaseIfExists() + { + $connection = m::mock(Connection::class); + $connection->shouldReceive('getSchemaGrammar')->once(); + + $builder = new SQLiteBuilder($connection); + + File::shouldReceive('exists') + ->once() + ->andReturn(true); + + File::shouldReceive('delete') + ->once() + ->with('my_temporary_database_b') + ->andReturn(true); + + $this->assertTrue($builder->dropDatabaseIfExists('my_temporary_database_b')); + + File::shouldReceive('exists') + ->once() + ->andReturn(false); + + $this->assertTrue($builder->dropDatabaseIfExists('my_temporary_database_c')); + + File::shouldReceive('exists') + ->once() + ->andReturn(true); + + File::shouldReceive('delete') + ->once() + ->with('my_temporary_database_c') + ->andReturn(false); + + $this->assertFalse($builder->dropDatabaseIfExists('my_temporary_database_c')); + } +} diff --git a/tests/Database/DatabaseSchemaBuilderTest.php b/tests/Database/DatabaseSchemaBuilderTest.php index 53a13c79128c..b22bfd7dc70e 100755 --- a/tests/Database/DatabaseSchemaBuilderTest.php +++ b/tests/Database/DatabaseSchemaBuilderTest.php @@ -4,6 +4,7 @@ use Illuminate\Database\Connection; use Illuminate\Database\Schema\Builder; +use LogicException; use Mockery as m; use PHPUnit\Framework\TestCase; use stdClass; @@ -15,6 +16,32 @@ protected function tearDown(): void m::close(); } + public function testCreateDatabase() + { + $connection = m::mock(Connection::class); + $grammar = m::mock(stdClass::class); + $connection->shouldReceive('getSchemaGrammar')->andReturn($grammar); + $builder = new Builder($connection); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('This database driver does not support creating databases.'); + + $builder->createDatabase('foo'); + } + + public function testDropDatabaseIfExists() + { + $connection = m::mock(Connection::class); + $grammar = m::mock(stdClass::class); + $connection->shouldReceive('getSchemaGrammar')->andReturn($grammar); + $builder = new Builder($connection); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('This database driver does not support dropping databases.'); + + $builder->dropDatabaseIfExists('foo'); + } + public function testHasTableCorrectlyCallsGrammar() { $connection = m::mock(Connection::class); diff --git a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php index 5ca3790522ab..72a072592b1a 100755 --- a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php +++ b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php @@ -894,6 +894,42 @@ public function testQuoteStringOnArray() $this->assertSame("N'中文', N'測試'", $this->getGrammar()->quoteString(['中文', '測試'])); } + public function testCreateDatabase() + { + $connection = $this->getConnection(); + + $statement = $this->getGrammar()->compileCreateDatabase('my_database_a', $connection); + + $this->assertSame( + 'create database "my_database_a"', + $statement + ); + + $statement = $this->getGrammar()->compileCreateDatabase('my_database_b', $connection); + + $this->assertSame( + 'create database "my_database_b"', + $statement + ); + } + + public function testDropDatabaseIfExists() + { + $statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_a'); + + $this->assertSame( + 'drop database if exists "my_database_a"', + $statement + ); + + $statement = $this->getGrammar()->compileDropDatabaseIfExists('my_database_b'); + + $this->assertSame( + 'drop database if exists "my_database_b"', + $statement + ); + } + protected function getConnection() { return m::mock(Connection::class); diff --git a/tests/Database/SqlServerBuilderTest.php b/tests/Database/SqlServerBuilderTest.php new file mode 100644 index 000000000000..de82f796056c --- /dev/null +++ b/tests/Database/SqlServerBuilderTest.php @@ -0,0 +1,46 @@ +shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('statement')->once()->with( + 'create database "my_temporary_database_a"' + )->andReturn(true); + + $builder = new SqlServerBuilder($connection); + $builder->createDatabase('my_temporary_database_a'); + } + + public function testDropDatabaseIfExists() + { + $grammar = new SqlServerGrammar(); + + $connection = m::mock(Connection::class); + $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); + $connection->shouldReceive('statement')->once()->with( + 'drop database if exists "my_temporary_database_b"' + )->andReturn(true); + + $builder = new SqlServerBuilder($connection); + + $builder->dropDatabaseIfExists('my_temporary_database_b'); + } +} diff --git a/tests/Testing/ParallelConsoleOutputTest.php b/tests/Testing/ParallelConsoleOutputTest.php new file mode 100644 index 000000000000..130b6ecada6a --- /dev/null +++ b/tests/Testing/ParallelConsoleOutputTest.php @@ -0,0 +1,25 @@ +write('Running phpunit in 12 processes with laravel/laravel.'); + $this->assertEmpty($original->fetch()); + + $output->write('Configuration read from phpunit.xml.dist'); + $this->assertEmpty($original->fetch()); + + $output->write('... 3/3 (100%)'); + $this->assertSame('... 3/3 (100%)', $original->fetch()); + } +} diff --git a/tests/Testing/ParallelTestingTest.php b/tests/Testing/ParallelTestingTest.php new file mode 100644 index 000000000000..25d41f7f1b5c --- /dev/null +++ b/tests/Testing/ParallelTestingTest.php @@ -0,0 +1,99 @@ +{$caller}($this); + $this->assertFalse($state); + + $parallelTesting->{$callback}(function ($token, $testCase = null) use ($callback, &$state) { + if (in_array($callback, ['setUpTestCase', 'tearDownTestCase'])) { + $this->assertSame($this, $testCase); + } else { + $this->assertNull($testCase); + } + + $this->assertEquals(1, $token); + $state = true; + }); + + $parallelTesting->{$caller}($this); + $this->assertFalse($state); + + $parallelTesting->resolveTokenUsing(function () { + return 1; + }); + + $parallelTesting->{$caller}($this); + $this->assertTrue($state); + } + + public function testOptions() + { + $parallelTesting = new ParallelTesting(Container::getInstance()); + + $this->assertFalse($parallelTesting->option('recreate_databases')); + + $parallelTesting->resolveOptionsUsing(function ($option) { + return $option == 'recreate_databases'; + }); + + $this->assertFalse($parallelTesting->option('recreate_caches')); + $this->assertTrue($parallelTesting->option('recreate_databases')); + } + + public function testToken() + { + $parallelTesting = new ParallelTesting(Container::getInstance()); + + $this->assertFalse($parallelTesting->token()); + + $parallelTesting->resolveTokenUsing(function () { + return 1; + }); + + $this->assertSame(1, $parallelTesting->token()); + } + + public function callbacks() + { + return [ + ['setUpProcess'], + ['setUpTestCase'], + ['tearDownTestCase'], + ['tearDownProcess'], + ]; + } + + public function tearDown(): void + { + parent::tearDown(); + + Container::setInstance(null); + + unset($_SERVER['LARAVEL_PARALLEL_TESTING']); + } +}