diff --git a/.github/workflows/databases.yml b/.github/workflows/databases.yml index f4457fd85c52..5f0c953cebe9 100644 --- a/.github/workflows/databases.yml +++ b/.github/workflows/databases.yml @@ -235,3 +235,40 @@ jobs: DB_DATABASE: master DB_USERNAME: SA DB_PASSWORD: Forge123 + + sqlite: + runs-on: ubuntu-20.04 + + strategy: + fail-fast: true + + name: SQLite + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.2 + extensions: dom, curl, libxml, mbstring, zip, pcntl, sqlsrv, pdo, pdo_sqlsrv, odbc, pdo_odbc, :php-psr + tools: composer:v2 + coverage: none + + - name: Install dependencies + uses: nick-fields/retry@v2 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer update --prefer-stable --prefer-dist --no-interaction --no-progress + + - name: Create database + run: vendor/bin/testbench package:create-sqlite-db + + - name: Execute tests + run: vendor/bin/phpunit tests/Integration/Database + env: + DB_CONNECTION: sqlite diff --git a/composer.json b/composer.json index 5a4093af6b56..0b9740065dbd 100644 --- a/composer.json +++ b/composer.json @@ -105,7 +105,7 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.6", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^9.0", + "orchestra/testbench-core": "dev-persist-db-connections", "pda/pheanstalk": "^5.0", "phpstan/phpstan": "^1.4.7", "phpunit/phpunit": "^10.5|^11.0", diff --git a/src/Illuminate/Contracts/Database/Connectors/ConnectionFactory.php b/src/Illuminate/Contracts/Database/Connectors/ConnectionFactory.php new file mode 100644 index 000000000000..b046db24624f --- /dev/null +++ b/src/Illuminate/Contracts/Database/Connectors/ConnectionFactory.php @@ -0,0 +1,15 @@ +parseConfig($config, $name); diff --git a/src/Illuminate/Database/DatabaseManager.php b/src/Illuminate/Database/DatabaseManager.php index 76680ff2f2d4..aa8b45917ade 100755 --- a/src/Illuminate/Database/DatabaseManager.php +++ b/src/Illuminate/Database/DatabaseManager.php @@ -2,7 +2,7 @@ namespace Illuminate\Database; -use Illuminate\Database\Connectors\ConnectionFactory; +use Illuminate\Contracts\Database\Connectors\ConnectionFactory; use Illuminate\Database\Events\ConnectionEstablished; use Illuminate\Support\Arr; use Illuminate\Support\ConfigurationUrlParser; @@ -31,7 +31,7 @@ class DatabaseManager implements ConnectionResolverInterface /** * The database connection factory instance. * - * @var \Illuminate\Database\Connectors\ConnectionFactory + * @var \Illuminate\Contracts\Database\Connectors\ConnectionFactory */ protected $factory; @@ -60,7 +60,7 @@ class DatabaseManager implements ConnectionResolverInterface * Create a new database manager instance. * * @param \Illuminate\Contracts\Foundation\Application $app - * @param \Illuminate\Database\Connectors\ConnectionFactory $factory + * @param \Illuminate\Contracts\Database\Connectors\ConnectionFactory $factory * @return void */ public function __construct($app, ConnectionFactory $factory) @@ -164,7 +164,7 @@ protected function makeConnection($name) return call_user_func($this->extensions[$driver], $config, $name); } - return $this->factory->make($config, $name); + return $this->factory->make($config, $name ?? $this->getDefaultConnection()); } /** diff --git a/src/Illuminate/Foundation/Bootstrap/OverrideProvidersForTesting.php b/src/Illuminate/Foundation/Bootstrap/OverrideProvidersForTesting.php new file mode 100644 index 000000000000..f25a284face8 --- /dev/null +++ b/src/Illuminate/Foundation/Bootstrap/OverrideProvidersForTesting.php @@ -0,0 +1,28 @@ +runningUnitTests()) { + return; + } + + if ($app->bound('db.factory')) { + tap($app['db.factory'], function ($factory) use ($app) { + $app->instance('db.factory', new DatabaseConnectionFactory($app, $factory)); + }); + } + } +} diff --git a/src/Illuminate/Foundation/Console/Kernel.php b/src/Illuminate/Foundation/Console/Kernel.php index 16fc49d87141..dc32018e22e5 100644 --- a/src/Illuminate/Foundation/Console/Kernel.php +++ b/src/Illuminate/Foundation/Console/Kernel.php @@ -121,6 +121,7 @@ class Kernel implements KernelContract \Illuminate\Foundation\Bootstrap\RegisterFacades::class, \Illuminate\Foundation\Bootstrap\SetRequestForConsole::class, \Illuminate\Foundation\Bootstrap\RegisterProviders::class, + \Illuminate\Foundation\Bootstrap\OverrideProvidersForTesting::class, \Illuminate\Foundation\Bootstrap\BootProviders::class, ]; diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php index 141504fda314..2d08152c5869 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php @@ -16,6 +16,13 @@ trait InteractsWithDatabase { + /** + * Determine whether database connection should be disconnected between tests. + * + * @var bool + */ + protected $disconnectDatabaseConnections = false; + /** * Assert that a given where condition exists in the database. * diff --git a/src/Illuminate/Foundation/Testing/DatabaseConnectionFactory.php b/src/Illuminate/Foundation/Testing/DatabaseConnectionFactory.php new file mode 100644 index 000000000000..1ec69d92b9b7 --- /dev/null +++ b/src/Illuminate/Foundation/Testing/DatabaseConnectionFactory.php @@ -0,0 +1,79 @@ + + */ + protected static array $cachedConnections = []; + + /** + * Create a new connection factory instance. + * + * @param \Illuminate\Contracts\Container\Container $container + * @return void + */ + public function __construct( + Container $container, + protected ConnectionFactoryContract $factory + ) { + parent::__construct($container); + } + + /** + * Establish a PDO connection based on the configuration. + * + * @param array $config + * @param string $name + * @return \Illuminate\Database\Connection + */ + #[\Override] + public function make(array $config, $name) + { + $key = $name ?? $config['name']; + + // In-Memory Databases doesn't have any max connections limitation so it should be safe to just create a new connection between tests. + // Because some tests may be depend on thier volatile, we should always create new connections to avoid carrying over previous data. + if ($config['driver'] === 'sqlite' && $config['database'] === ':memory:') { + return $this->factory->make($config, $name); + } + + if (! isset(static::$cachedConnections[$key]) || is_null(static::$cachedConnections[$key]->getRawPdo() ?? null)) { + return static::$cachedConnections[$key] = $this->factory->make($config, $name); + } + + $config = $this->parseConfig($config, $name); + + $connection = $this->createConnection( + $config['driver'], static::$cachedConnections[$key]->getRawPdo(), $config['database'], $config['prefix'], $config + )->setReadPdo(static::$cachedConnections[$key]->getRawReadPdo()); + + return static::$cachedConnections[$key] = $connection; + } + + /** + * Flush the current state. + * + * @return void + */ + public static function flushState(): void + { + foreach (static::$cachedConnections as $connection) { + $connection->disconnect(); + } + + static::$cachedConnections = []; + RefreshDatabaseState::$inMemoryConnections = []; + } +} diff --git a/src/Illuminate/Foundation/Testing/DatabaseTransactions.php b/src/Illuminate/Foundation/Testing/DatabaseTransactions.php index 83a686f3558c..113f863f7e35 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTransactions.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTransactions.php @@ -33,7 +33,6 @@ public function beginDatabaseTransaction() $connection->unsetEventDispatcher(); $connection->rollBack(); $connection->setEventDispatcher($dispatcher); - $connection->disconnect(); } }); } diff --git a/src/Illuminate/Foundation/Testing/DatabaseTransactionsManager.php b/src/Illuminate/Foundation/Testing/DatabaseTransactionsManager.php index bc5450486d48..1f20a81cbf28 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTransactionsManager.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTransactionsManager.php @@ -12,6 +12,7 @@ class DatabaseTransactionsManager extends BaseManager * @param callable $callback * @return void */ + #[\Override] public function addCallback($callback) { // If there are no transactions, we'll run the callbacks right away. Also, we'll run it @@ -29,6 +30,7 @@ public function addCallback($callback) * * @return \Illuminate\Support\Collection */ + #[\Override] public function callbackApplicableTransactions() { return $this->pendingTransactions->skip(1)->values(); @@ -40,6 +42,7 @@ public function callbackApplicableTransactions() * @param int $level * @return bool */ + #[\Override] public function afterCommitCallbacksShouldBeExecuted($level) { return $level === 1; diff --git a/src/Illuminate/Foundation/Testing/RefreshDatabase.php b/src/Illuminate/Foundation/Testing/RefreshDatabase.php index 4c4e084ab0fa..66d86d2aacd9 100644 --- a/src/Illuminate/Foundation/Testing/RefreshDatabase.php +++ b/src/Illuminate/Foundation/Testing/RefreshDatabase.php @@ -108,7 +108,6 @@ public function beginDatabaseTransaction() $connection->unsetEventDispatcher(); $connection->rollBack(); $connection->setEventDispatcher($dispatcher); - $connection->disconnect(); } }); } diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index 93805335b8ff..5323dba28f9a 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -198,6 +198,13 @@ protected function tearDown(): void ParallelTesting::callTearDownTestCaseCallbacks($this); + if ( + property_exists($this, 'disconnectDatabaseConnections') + && $this->disconnectDatabaseConnections === true + ) { + DatabaseConnectionFactory::flushState(); + } + $this->app->flush(); $this->app = null; @@ -266,6 +273,7 @@ protected function tearDown(): void public static function tearDownAfterClass(): void { static::$latestResponse = null; + DatabaseConnectionFactory::flushState(); foreach ([ \PHPUnit\Util\Annotation\Registry::class, diff --git a/tests/Integration/Database/DatabaseTestCase.php b/tests/Integration/Database/DatabaseTestCase.php index 14f78bd71e0a..f72fc31a5c4d 100644 --- a/tests/Integration/Database/DatabaseTestCase.php +++ b/tests/Integration/Database/DatabaseTestCase.php @@ -16,17 +16,6 @@ abstract class DatabaseTestCase extends TestCase */ protected $driver; - protected function setUp(): void - { - $this->beforeApplicationDestroyed(function () { - foreach (array_keys($this->app['db']->getConnections()) as $name) { - $this->app['db']->purge($name); - } - }); - - parent::setUp(); - } - protected function defineEnvironment($app) { $connection = $app['config']->get('database.default'); diff --git a/tests/Integration/Database/EloquentBelongsToManyTest.php b/tests/Integration/Database/EloquentBelongsToManyTest.php index 7f77db612656..537c1267a991 100644 --- a/tests/Integration/Database/EloquentBelongsToManyTest.php +++ b/tests/Integration/Database/EloquentBelongsToManyTest.php @@ -15,13 +15,6 @@ class EloquentBelongsToManyTest extends DatabaseTestCase { - protected function tearDown(): void - { - parent::tearDown(); - - Carbon::setTestNow(null); - } - protected function afterRefreshingDatabase() { Schema::create('users', function (Blueprint $table) { diff --git a/tests/Integration/Database/EloquentMassPrunableTest.php b/tests/Integration/Database/EloquentMassPrunableTest.php index e3e1c0c36f34..9a998a0ae834 100644 --- a/tests/Integration/Database/EloquentMassPrunableTest.php +++ b/tests/Integration/Database/EloquentMassPrunableTest.php @@ -93,15 +93,6 @@ public function testPrunesSoftDeletedRecords() $this->assertEquals(0, MassPrunableSoftDeleteTestModel::count()); $this->assertEquals(2000, MassPrunableSoftDeleteTestModel::withTrashed()->count()); } - - protected function tearDown(): void - { - parent::tearDown(); - - Container::setInstance(null); - - m::close(); - } } class MassPrunableTestModel extends Model diff --git a/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingDatabaseTransactionsTest.php b/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingDatabaseTransactionsTest.php index 25a8b27a72cc..d948090adf77 100644 --- a/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingDatabaseTransactionsTest.php +++ b/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingDatabaseTransactionsTest.php @@ -19,12 +19,6 @@ class EloquentTransactionWithAfterCommitUsingDatabaseTransactionsTest extends Te protected function setUp(): void { - $this->beforeApplicationDestroyed(function () { - foreach (array_keys($this->app['db']->getConnections()) as $name) { - $this->app['db']->purge($name); - } - }); - parent::setUp(); if ($this->usesSqliteInMemoryDatabaseConnection()) { diff --git a/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingRefreshDatabaseTest.php b/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingRefreshDatabaseTest.php index 21bb8291d480..38e830b921f1 100644 --- a/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingRefreshDatabaseTest.php +++ b/tests/Integration/Database/EloquentTransactionWithAfterCommitUsingRefreshDatabaseTest.php @@ -17,17 +17,6 @@ class EloquentTransactionWithAfterCommitUsingRefreshDatabaseTest extends TestCas */ protected $driver; - protected function setUp(): void - { - $this->beforeApplicationDestroyed(function () { - foreach (array_keys($this->app['db']->getConnections()) as $name) { - $this->app['db']->purge($name); - } - }); - - parent::setUp(); - } - protected function getEnvironmentSetUp($app) { $connection = $app['config']->get('database.default'); diff --git a/tests/Integration/Database/SchemaBuilderTest.php b/tests/Integration/Database/SchemaBuilderTest.php index e1fb45a26267..608b4ec16a1b 100644 --- a/tests/Integration/Database/SchemaBuilderTest.php +++ b/tests/Integration/Database/SchemaBuilderTest.php @@ -6,7 +6,9 @@ use Illuminate\Database\Schema\Grammars\SQLiteGrammar; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use Orchestra\Testbench\Attributes\DisconnectDatabaseConnections; +#[DisconnectDatabaseConnections] class SchemaBuilderTest extends DatabaseTestCase { protected function destroyDatabaseMigrations()