diff --git a/docs/todo.md b/docs/todo.md index 3f5f03920f..c49f8af1bf 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -30,6 +30,7 @@ ## Testing +- Make parallel database isolation URL-aware. A persistent connection configured only with `url` currently skips worker-database rewriting, so ParaTest workers share one database. Normalize the connection before deciding whether it can be managed, keep in-memory SQLite process-local, and either rewrite supported persistent URLs per worker or fail with a clear error when automatic isolation is impossible. - Port current Laravel's complete `tests/Support/SupportTestingEventFakeTest.php`, preserving Hypervel-specific EventFake coverage and coroutine-safe test behavior. - Complete Testing assertion coverage: port the remaining current Laravel `TestResponseTest` cases through the incremental upstream-update workflow, and add focused coverage for `TestView`'s public assertion and string surface where Laravel has no equivalent suite. - Add the repository-required `: void` return type to the remaining untyped HTTP test methods: 176 in `tests/Http/HttpClientTest.php`, 30 in `tests/Http/HttpRequestTrustedStateTest.php`, and 4 in `tests/Http/HttpRequestTrustedStateCoroutineTest.php`. Verify each file after the mechanical conversion. diff --git a/src/database/src/SQLiteDatabase.php b/src/database/src/SQLiteDatabase.php index 46775c9ef6..3e0771f2b0 100644 --- a/src/database/src/SQLiteDatabase.php +++ b/src/database/src/SQLiteDatabase.php @@ -4,6 +4,8 @@ namespace Hypervel\Database; +use InvalidArgumentException; + class SQLiteDatabase { /** @@ -41,4 +43,22 @@ public static function isInMemory(string $database): bool return ($parameters['mode'] ?? null) === 'memory'; } + + /** + * Determine if a connection configuration resolves to an in-memory SQLite database. + * + * Discrete configurations pass through normalization unchanged. Malformed URLs + * intentionally fail with the database configuration parser's exception. + * + * @throws InvalidArgumentException + */ + public static function isInMemoryConfiguration(array $configuration): bool + { + $configuration = (new ConfigurationUrlParser)->parseConfiguration($configuration); + $database = $configuration['database'] ?? null; + + return ($configuration['driver'] ?? null) === 'sqlite' + && is_string($database) + && static::isInMemory($database); + } } diff --git a/src/foundation/src/Testing/Concerns/InteractsWithParallelDatabase.php b/src/foundation/src/Testing/Concerns/InteractsWithParallelDatabase.php index c89ec0447e..492e7ea632 100644 --- a/src/foundation/src/Testing/Concerns/InteractsWithParallelDatabase.php +++ b/src/foundation/src/Testing/Concerns/InteractsWithParallelDatabase.php @@ -78,7 +78,8 @@ protected function configureParallelDatabaseName($app): void * Ensure the per-worker database exists, creating it if needed. * * Called from database testing traits (RefreshDatabase, DatabaseMigrations, - * DatabaseTransactions) after the app is booted and connections are available. + * DatabaseTruncation, DatabaseTransactions) after the app is booted and + * connections are available. * The config has already been rewritten by configureParallelDatabaseName(). * * No-op when not running in parallel or when using in-memory SQLite. diff --git a/src/foundation/src/Testing/DatabaseTruncation.php b/src/foundation/src/Testing/DatabaseTruncation.php index c8ce9a8a02..ecc142403c 100644 --- a/src/foundation/src/Testing/DatabaseTruncation.php +++ b/src/foundation/src/Testing/DatabaseTruncation.php @@ -5,14 +5,21 @@ namespace Hypervel\Foundation\Testing; use Hypervel\Contracts\Console\Kernel; +use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Database\ConnectionInterface; +use Hypervel\Database\SQLiteDatabase; +use Hypervel\Foundation\Testing\Concerns\InteractsWithParallelDatabase; use Hypervel\Foundation\Testing\Traits\CanConfigureMigrationCommands; use Hypervel\Support\Arr; use Hypervel\Support\Collection; +/** + * This concern is mutually exclusive with RefreshDatabase and DatabaseMigrations. + */ trait DatabaseTruncation { use CanConfigureMigrationCommands; + use InteractsWithParallelDatabase; /** * The cached names of the database tables for each connection. @@ -24,6 +31,9 @@ trait DatabaseTruncation */ protected function truncateDatabaseTables(): void { + $this->ensureParallelDatabaseExists(); + $this->restoreInMemoryDatabases(); + $this->beforeTruncatingDatabase(); // Migrate and seed the database on first run... @@ -32,6 +42,8 @@ protected function truncateDatabaseTables(): void $this->app->make(Kernel::class)->setArtisan(null); + $this->cacheInMemoryDatabases(); + RefreshDatabaseState::$migrated = true; return; @@ -51,6 +63,76 @@ protected function truncateDatabaseTables(): void $this->afterTruncatingDatabase(); } + /** + * Restore the in-memory databases between tests. + */ + protected function restoreInMemoryDatabases(): void + { + if (RefreshDatabaseState::$inMemoryConnections === []) { + return; + } + + $database = $this->app->make('db'); + $defaultConnection = $this->app->make('config')->string('database.default'); + + foreach ($this->connectionsToTruncate() as $name) { + $connectionName = $name ?? $defaultConnection; + + if (isset(RefreshDatabaseState::$inMemoryConnections[$connectionName])) { + // The PDO outlives its original application; the dispatcher must not. + $database->connection($name) + ->setPdo(RefreshDatabaseState::$inMemoryConnections[$connectionName]) + ->setEventDispatcher($this->app->make(Dispatcher::class)); + } + } + } + + /** + * Cache the in-memory databases after migration. + */ + protected function cacheInMemoryDatabases(): void + { + $database = $this->app->make('db'); + $config = $this->app->make('config'); + $defaultConnection = $config->string('database.default'); + + foreach ($this->connectionsToTruncate() as $name) { + if ($this->usingInMemoryDatabaseForTruncation($name)) { + $connectionName = $name ?? $defaultConnection; + + RefreshDatabaseState::$inMemoryConnections[$connectionName] + = $database->connection($name)->getPdo(); + } + } + } + + /** + * Determine if any connection being truncated uses an in-memory database. + */ + protected function usingInMemoryDatabasesForTruncation(): bool + { + foreach ($this->connectionsToTruncate() as $name) { + if ($this->usingInMemoryDatabaseForTruncation($name)) { + return true; + } + } + + return false; + } + + /** + * Determine if the given connection uses an in-memory database. + */ + protected function usingInMemoryDatabaseForTruncation(?string $name): bool + { + $config = $this->app->make('config'); + $name ??= $config->string('database.default'); + $configuration = $config->get("database.connections.{$name}"); + + return is_array($configuration) + && SQLiteDatabase::isInMemoryConfiguration($configuration); + } + /** * Truncate the database tables for all configured connections. */ diff --git a/src/foundation/src/Testing/RefreshDatabase.php b/src/foundation/src/Testing/RefreshDatabase.php index c2d4fe0421..d462744e77 100644 --- a/src/foundation/src/Testing/RefreshDatabase.php +++ b/src/foundation/src/Testing/RefreshDatabase.php @@ -93,11 +93,10 @@ protected function usingInMemoryDatabase(?string $name = null): bool { $config = $this->app->make('config'); $name ??= $this->getRefreshConnection(); + $configuration = $config->get("database.connections.{$name}"); - // All supported SQLite memory URI forms need the same refresh lifecycle. - return SQLiteDatabase::isInMemory( - $config->string("database.connections.{$name}.database") - ); + return is_array($configuration) + && SQLiteDatabase::isInMemoryConfiguration($configuration); } /** diff --git a/src/testbench/src/Concerns/HandlesDatabases.php b/src/testbench/src/Concerns/HandlesDatabases.php index 945a4f19cf..82a0caf2db 100644 --- a/src/testbench/src/Concerns/HandlesDatabases.php +++ b/src/testbench/src/Concerns/HandlesDatabases.php @@ -77,14 +77,10 @@ protected function usesSqliteInMemoryDatabaseConnection(?string $connection = nu $connection ??= $config->get('database.default'); - /** @var null|array{driver: string, database: string} $database */ - $database = $config->get("database.connections.{$connection}"); + $configuration = $config->get("database.connections.{$connection}"); - if ($database === null || $database['driver'] !== 'sqlite') { - return false; - } - - return SQLiteDatabase::isInMemory($database['database']); + return is_array($configuration) + && SQLiteDatabase::isInMemoryConfiguration($configuration); } /** diff --git a/src/testbench/src/Concerns/InteractsWithMigrations.php b/src/testbench/src/Concerns/InteractsWithMigrations.php index e5d69a0fc9..15880df8c2 100644 --- a/src/testbench/src/Concerns/InteractsWithMigrations.php +++ b/src/testbench/src/Concerns/InteractsWithMigrations.php @@ -9,6 +9,7 @@ use Hypervel\Database\Migrations\Migrator; use Hypervel\Foundation\Console\Kernel as FoundationConsoleKernel; use Hypervel\Foundation\Testing\DatabaseMigrations; +use Hypervel\Foundation\Testing\DatabaseTruncation; use Hypervel\Foundation\Testing\RefreshDatabaseState; use Hypervel\Support\Arr; use Hypervel\Testbench\Attributes\ResetRefreshDatabaseState; @@ -31,7 +32,7 @@ trait InteractsWithMigrations protected function setUpInteractsWithMigrations(): void { - if ($this->usesSqliteInMemoryDatabaseConnection()) { + if ($this->usesInMemoryDatabaseForMigrationState()) { $this->afterApplicationCreated(static function (): void { static::usesTestingFeature(new ResetRefreshDatabaseState); }); @@ -41,6 +42,9 @@ protected function setUpInteractsWithMigrations(): void protected function tearDownInteractsWithMigrations(): void { $hasInMemoryConnections = ! empty(RefreshDatabaseState::$inMemoryConnections); + $preservesInMemoryDatabase = static::usesTestingConcern(DatabaseTruncation::class) + && ! static::usesTestingConcern(DatabaseMigrations::class) + && ! static::usesRefreshDatabaseTestingConcern(); $processors = $this->cachedTestMigratorProcessors; $this->cachedTestMigratorProcessors = []; $failure = null; @@ -48,7 +52,11 @@ protected function tearDownInteractsWithMigrations(): void try { if ( (count($processors) > 0 && static::usesRefreshDatabaseTestingConcern()) - || ($hasInMemoryConnections && $this->usesSqliteInMemoryDatabaseConnection()) + || ( + ! $preservesInMemoryDatabase + && $hasInMemoryConnections + && $this->usesInMemoryDatabaseForMigrationState() + ) ) { ResetRefreshDatabaseState::run(); } @@ -79,9 +87,37 @@ protected function loadMigrationsFrom(array|string $paths): void /** @var ApplicationContract $app */ $app = $this->app; + if ( + (is_string($paths) || Arr::isList($paths)) + && $this->shouldRegisterMigrationPaths() + ) { + /** @var list|string $paths */ + load_migration_paths($app, $paths); + + return; + } + + /** @var array|string $paths */ + $this->runMigrationProcessor($app, $this->resolvePackageMigrationsOptions($paths)); + } + + /** + * Determine whether migration paths should be registered for an upcoming migration. + */ + protected function shouldRegisterMigrationPaths(): bool + { $migrateRefresh = property_exists($this, 'migrateRefresh') && (bool) $this->migrateRefresh; - $refreshesDatabase = static::usesTestingConcern(DatabaseMigrations::class) + + // List and string paths target the default connection; named options use a processor. + return static::usesTestingConcern(DatabaseMigrations::class) + || ( + static::usesTestingConcern(DatabaseTruncation::class) + && ( + RefreshDatabaseState::$migrated === false + || $this->usesSqliteInMemoryDatabaseConnection() + ) + ) || ( static::usesRefreshDatabaseTestingConcern() && ( @@ -92,19 +128,6 @@ protected function loadMigrationsFrom(array|string $paths): void ) ) ); - - if ( - (is_string($paths) || Arr::isList($paths)) - && $refreshesDatabase - ) { - /** @var list|string $paths */ - load_migration_paths($app, $paths); - - return; - } - - /** @var array|string $paths */ - $this->runMigrationProcessor($app, $this->resolvePackageMigrationsOptions($paths)); } /** @@ -201,6 +224,19 @@ protected function runMigrationProcessor(ApplicationContract $app, array $option $this->resetApplicationArtisanCommands($app); } + /** + * Determine whether the active database concern retains in-memory state. + */ + protected function usesInMemoryDatabaseForMigrationState(): bool + { + if (static::usesTestingConcern(DatabaseTruncation::class)) { + // Every cached truncation PDO needs the same class-boundary state owner. + return $this->usingInMemoryDatabasesForTruncation(); /* @phpstan-ignore method.notFound */ + } + + return $this->usesSqliteInMemoryDatabaseConnection(); + } + protected function resetApplicationArtisanCommands(ApplicationContract $app): void { $kernel = $app->make(ConsoleKernelContract::class); diff --git a/src/testbench/src/Concerns/WithHypervelMigrations.php b/src/testbench/src/Concerns/WithHypervelMigrations.php index 6e8086680b..45074101a3 100644 --- a/src/testbench/src/Concerns/WithHypervelMigrations.php +++ b/src/testbench/src/Concerns/WithHypervelMigrations.php @@ -4,9 +4,6 @@ namespace Hypervel\Testbench\Concerns; -use Hypervel\Foundation\Testing\DatabaseMigrations; -use Hypervel\Foundation\Testing\RefreshDatabaseState; - use function Hypervel\Testbench\after_resolving; use function Hypervel\Testbench\default_migration_path; @@ -25,21 +22,7 @@ protected function setUpWithHypervelMigrations(): void return; } - $migrateRefresh = property_exists($this, 'migrateRefresh') - && (bool) $this->migrateRefresh; - $refreshesDatabase = static::usesTestingConcern(DatabaseMigrations::class) - || ( - static::usesRefreshDatabaseTestingConcern() - && ( - $migrateRefresh - || ( - RefreshDatabaseState::$migrated === false - && RefreshDatabaseState::$lazilyRefreshed === false - ) - ) - ); - - if ($refreshesDatabase) { + if ($this->shouldRegisterMigrationPaths()) { after_resolving($this->app, 'migrator', static function ($migrator, $app): void { $migrator->path(default_migration_path()); }); diff --git a/src/testbench/src/TestCase.php b/src/testbench/src/TestCase.php index 9f0fe79457..fa8ecc3c60 100644 --- a/src/testbench/src/TestCase.php +++ b/src/testbench/src/TestCase.php @@ -10,8 +10,12 @@ use Hypervel\Filesystem\Filesystem; use Hypervel\Foundation\Testing\DatabaseMigrations; use Hypervel\Foundation\Testing\DatabaseTransactions; +use Hypervel\Foundation\Testing\DatabaseTruncation; use Hypervel\Foundation\Testing\RefreshDatabase; use Hypervel\Foundation\Testing\TestCase as BaseTestCase; +use Hypervel\Testbench\Attributes\ResetRefreshDatabaseState; +use Hypervel\Testbench\Attributes\WithMigration; +use ReflectionMethod; use RuntimeException; use Swoole\Timer; use Throwable; @@ -24,6 +28,7 @@ * * @method void refreshDatabase() * @method void runDatabaseMigrations() + * @method void truncateDatabaseTables() * @method void beginDatabaseTransaction() * @method void disableMiddlewareForAllTests() * @method void disableEventsForAllTests() @@ -133,6 +138,9 @@ protected function preservePackageManifestCache(): void */ protected function setUpDatabaseTraits(array $uses): void { + // Reset before database attributes register paths against retained schema state. + $this->prepareDatabaseTruncationForMethod($uses); + $this->setUpDatabaseRequirements(function () use ($uses): void { if (isset($uses[RefreshDatabase::class])) { $this->refreshDatabase(); @@ -141,6 +149,10 @@ protected function setUpDatabaseTraits(array $uses): void if (isset($uses[DatabaseMigrations::class])) { $this->runDatabaseMigrations(); } + + if (isset($uses[DatabaseTruncation::class])) { + $this->truncateDatabaseTables(); + } }); if (isset($uses[DatabaseTransactions::class])) { @@ -148,6 +160,29 @@ protected function setUpDatabaseTraits(array $uses): void } } + /** + * Isolate method-specific migration sets from the retained truncation schema. + */ + protected function prepareDatabaseTruncationForMethod(array $uses): void + { + if (! isset($uses[DatabaseTruncation::class])) { + return; + } + + $method = $this->resolvePhpUnitTestMethodName(); + + if ($method === null + || (new ReflectionMethod(static::class, $method))->getAttributes(WithMigration::class) === []) { + return; + } + + ResetRefreshDatabaseState::run(); + + $this->beforeApplicationDestroyed(static function (): void { + ResetRefreshDatabaseState::run(); + }); + } + /** * Refresh the application instance. */ diff --git a/tests/Database/SQLiteDatabaseTest.php b/tests/Database/SQLiteDatabaseTest.php index 8fb740fe9b..24ccc3a736 100644 --- a/tests/Database/SQLiteDatabaseTest.php +++ b/tests/Database/SQLiteDatabaseTest.php @@ -61,4 +61,28 @@ public static function memoryProvider(): array 'mixed-case mode value is not memory' => ['file:database?mode=MEMORY', false], ]; } + + #[DataProvider('configurationProvider')] + public function testItClassifiesInMemoryConnectionConfigurations(array $configuration, bool $expected): void + { + $this->assertSame($expected, SQLiteDatabase::isInMemoryConfiguration($configuration)); + } + + /** + * @return array, bool}> + */ + public static function configurationProvider(): array + { + return [ + 'discrete SQLite memory' => [['driver' => 'sqlite', 'database' => ':memory:'], true], + 'SQLite memory URL' => [['url' => 'sqlite:///:memory:'], true], + 'URL overrides discrete values' => [[ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'url' => 'mysql://root:secret@database/app', + ], false], + 'non-SQLite memory name' => [['driver' => 'mysql', 'database' => ':memory:'], false], + 'incomplete configuration' => [['driver' => 'sqlite'], false], + ]; + } } diff --git a/tests/Foundation/Testing/DatabaseTruncationTest.php b/tests/Foundation/Testing/DatabaseTruncationTest.php index 790657717d..0a5a616978 100644 --- a/tests/Foundation/Testing/DatabaseTruncationTest.php +++ b/tests/Foundation/Testing/DatabaseTruncationTest.php @@ -8,12 +8,15 @@ use Hypervel\Container\Container; use Hypervel\Contracts\Events\Dispatcher; use Hypervel\Database\Connection; +use Hypervel\Database\DatabaseManager; use Hypervel\Database\Query\Builder as QueryBuilder; use Hypervel\Database\Schema\Builder; use Hypervel\Database\Schema\PostgresBuilder; use Hypervel\Foundation\Testing\DatabaseTruncation; +use Hypervel\Foundation\Testing\RefreshDatabaseState; use Hypervel\Tests\TestCase; use Mockery as m; +use PDO; class DatabaseTruncationTest extends TestCase { @@ -25,6 +28,8 @@ class DatabaseTruncationTest extends TestCase private ?array $exceptTables = null; + private array $connectionsToTruncate = [null]; + protected function setUp(): void { parent::setUp(); @@ -44,8 +49,10 @@ protected function tearDown(): void { $this->app = null; static::$allTables = []; + RefreshDatabaseState::$inMemoryConnections = []; $this->tablesToTruncate = null; $this->exceptTables = null; + $this->connectionsToTruncate = [null]; parent::tearDown(); } @@ -181,6 +188,73 @@ public function testTruncateTablesOnPgsqlWithSearchPath() $this->assertEquals(['public.foo', 'public.bar', 'my_schema.foo', 'my_schema.baz'], $truncatedTables); } + public function testRestoreSkipsDatabaseResolutionWhenNoInMemoryConnectionIsCached(): void + { + $this->restoreInMemoryDatabases(); + + $this->assertFalse($this->app->resolved('db')); + } + + public function testCachesAndRestoresConfiguredInMemoryConnections(): void + { + $defaultPdo = m::mock(PDO::class); + $namedPdo = m::mock(PDO::class); + $sourceDefault = m::mock(Connection::class); + $sourceNamed = m::mock(Connection::class); + $sourceDefault->shouldReceive('getPdo')->once()->andReturn($defaultPdo); + $sourceNamed->shouldReceive('getPdo')->once()->andReturn($namedPdo); + + $sourceDatabase = m::mock(DatabaseManager::class); + $sourceDatabase->shouldReceive('connection')->once()->with(null)->andReturn($sourceDefault); + $sourceDatabase->shouldReceive('connection')->once()->with('named')->andReturn($sourceNamed); + $sourceDatabase->shouldNotReceive('connection')->with('file'); + + $this->app->instance('config', new Repository([ + 'database' => [ + 'default' => 'default', + 'connections' => [ + 'default' => ['driver' => 'sqlite', 'database' => ':memory:'], + 'named' => ['driver' => 'sqlite', 'database' => 'file::memory:?cache=shared'], + 'file' => ['driver' => 'sqlite', 'database' => '/tmp/database.sqlite'], + ], + ], + ])); + $this->app->instance('db', $sourceDatabase); + $this->connectionsToTruncate = [null, 'named', 'file']; + + $this->cacheInMemoryDatabases(); + + $dispatcher = m::mock(Dispatcher::class); + $restoredDefault = m::mock(Connection::class); + $restoredNamed = m::mock(Connection::class); + $restoredDefault->shouldReceive('setPdo')->once()->with($defaultPdo)->andReturnSelf(); + $restoredDefault->shouldReceive('setEventDispatcher')->once()->with($dispatcher)->andReturnSelf(); + $restoredNamed->shouldReceive('setPdo')->once()->with($namedPdo)->andReturnSelf(); + $restoredNamed->shouldReceive('setEventDispatcher')->once()->with($dispatcher)->andReturnSelf(); + + $restoredDatabase = m::mock(DatabaseManager::class); + $restoredDatabase->shouldReceive('connection')->once()->with(null)->andReturn($restoredDefault); + $restoredDatabase->shouldReceive('connection')->once()->with('named')->andReturn($restoredNamed); + $restoredDatabase->shouldNotReceive('connection')->with('file'); + + $this->app->instance('db', $restoredDatabase); + $this->app->instance(Dispatcher::class, $dispatcher); + + $this->restoreInMemoryDatabases(); + + $this->connectionsToTruncate = ['named', 'file']; + + $this->assertTrue($this->usingInMemoryDatabasesForTruncation()); + + $this->connectionsToTruncate = ['missing']; + + $this->assertFalse($this->usingInMemoryDatabasesForTruncation()); + $this->assertSame([ + 'default' => $defaultPdo, + 'named' => $namedPdo, + ], RefreshDatabaseState::$inMemoryConnections); + } + private function arrangeConnection( ?array &$actual, array $allTables, diff --git a/tests/Foundation/Testing/RefreshDatabaseTest.php b/tests/Foundation/Testing/RefreshDatabaseTest.php index 1d469a18d5..9df6390f0d 100644 --- a/tests/Foundation/Testing/RefreshDatabaseTest.php +++ b/tests/Foundation/Testing/RefreshDatabaseTest.php @@ -224,7 +224,7 @@ public function testRefreshTestDatabaseRestoresMockConsoleOutputAfterMigrationFa } } - public function testBeginDatabaseTransactionWorkSetsMigratedAndCachesPdoTogether() + public function testBeginDatabaseTransactionWorkSetsMigratedAndCachesPdoTogether(): void { // Regression test for the RefreshDatabase + RunTestsInCoroutine + // mid-setUp skip bug. The invariant the fix establishes is that @@ -254,7 +254,7 @@ public function testBeginDatabaseTransactionWorkSetsMigratedAndCachesPdoTogether 'database' => [ 'default' => 'default', 'connections' => [ - 'default' => ['database' => ':memory:'], + 'default' => ['driver' => 'sqlite', 'database' => ':memory:'], ], ], ])); @@ -291,7 +291,7 @@ public function testRestoreInMemoryDatabaseUsesResolvedDefaultConnectionName(): 'database' => [ 'default' => 'default', 'connections' => [ - 'default' => ['database' => ':memory:'], + 'default' => ['driver' => 'sqlite', 'database' => ':memory:'], ], ], ])); @@ -309,10 +309,12 @@ public function testInMemoryClassificationUsesTheNamedConnectionAndLiveDefault() 'default' => 'file', 'connections' => [ 'file' => [ + 'driver' => 'sqlite', 'database' => ParallelTesting::tempDir('RefreshDatabaseTest') . '/database.sqlite', ], - 'memory' => ['database' => 'file::memory:?cache=shared'], + 'memory' => ['driver' => 'sqlite', 'database' => 'file::memory:?cache=shared'], + 'url_memory' => ['url' => 'sqlite:///:memory:'], ], ], ])); @@ -320,6 +322,7 @@ public function testInMemoryClassificationUsesTheNamedConnectionAndLiveDefault() $this->assertFalse($this->usingInMemoryDatabase()); $this->assertFalse($this->usingInMemoryDatabase('file')); $this->assertTrue($this->usingInMemoryDatabase('memory')); + $this->assertTrue($this->usingInMemoryDatabase('url_memory')); $this->connectionsToTransact = ['file', 'memory']; @@ -362,10 +365,11 @@ public function testBeginDatabaseTransactionWorkCachesOnlyNamedInMemoryConnectio 'default' => 'file', 'connections' => [ 'file' => [ + 'driver' => 'sqlite', 'database' => ParallelTesting::tempDir('RefreshDatabaseTest') . '/database.sqlite', ], - 'memory' => ['database' => 'file::memory:?cache=shared'], + 'memory' => ['driver' => 'sqlite', 'database' => 'file::memory:?cache=shared'], ], ], ])); diff --git a/tests/Testbench/Databases/DatabaseTruncationExistingDatabaseTest.php b/tests/Testbench/Databases/DatabaseTruncationExistingDatabaseTest.php new file mode 100644 index 0000000000..88287dd845 --- /dev/null +++ b/tests/Testbench/Databases/DatabaseTruncationExistingDatabaseTest.php @@ -0,0 +1,68 @@ +ensureDirectoryExists(self::$databaseDirectory); + $files->put(self::$databaseDirectory . '/database.sqlite', ''); + + $app->make('config')->set( + 'database.connections.testing.database', + self::$databaseDirectory . '/database.sqlite', + ); + } + + #[Override] + protected function defineDatabaseMigrations(): void + { + RefreshDatabaseState::$migrated = true; + + $this->loadMigrationsFrom(workbench_path('database/migrations')); + } + + #[Test] + public function itRunsNewMigrationPathsWhenThePersistentDatabaseWasAlreadyMigrated(): void + { + $this->assertCount(1, $this->cachedTestMigratorProcessors); + $this->assertTrue(Schema::hasTable('testbench_users')); + } + + #[Override] + protected function tearDown(): void + { + try { + parent::tearDown(); + + $this->assertTrue(RefreshDatabaseState::$migrated); + } finally { + (new Filesystem)->deleteDirectory(self::$databaseDirectory); + } + } +} diff --git a/tests/Testbench/Databases/DatabaseTruncationWithMethodMigrationTest.php b/tests/Testbench/Databases/DatabaseTruncationWithMethodMigrationTest.php new file mode 100644 index 0000000000..8a19b4faeb --- /dev/null +++ b/tests/Testbench/Databases/DatabaseTruncationWithMethodMigrationTest.php @@ -0,0 +1,49 @@ +loadMigrationsFrom(workbench_path('database/migrations')); + } + + #[Test] + #[WithMigration('notifications')] + public function itIsolatesMethodSpecificMigrationSets(): void + { + $this->assertTrue(Schema::hasTable('notifications')); + } + + #[Test] + #[Depends('itIsolatesMethodSpecificMigrationSets')] + public function itRestoresTheClassSchemaAfterAMethodSpecificMigrationSet(): void + { + $this->assertTrue(Schema::hasTable('testbench_users')); + $this->assertFalse(Schema::hasTable('notifications')); + } +} diff --git a/tests/Testbench/Databases/MigrateWithHypervelMigrationsUsingDatabaseTruncationTest.php b/tests/Testbench/Databases/MigrateWithHypervelMigrationsUsingDatabaseTruncationTest.php new file mode 100644 index 0000000000..f02dfb4081 --- /dev/null +++ b/tests/Testbench/Databases/MigrateWithHypervelMigrationsUsingDatabaseTruncationTest.php @@ -0,0 +1,84 @@ +loadMigrationsFrom(workbench_path('database/migrations')); + } + + protected function afterTruncatingDatabase(): void + { + $this->truncated = true; + } + + #[Test] + #[DataProvider('applicationLifecycles')] + public function itMigratesOnceAndThenTruncatesAcrossApplicationLifecycles(int $lifecycle): void + { + $this->assertSame([], $this->cachedTestMigratorProcessors); + $this->assertTrue(RefreshDatabaseState::$migrated); + $this->assertSame($lifecycle === 2, $this->truncated); + $this->assertTrue(Schema::hasTable('users')); + $this->assertTrue(Schema::hasTable('testbench_users')); + $this->assertSame(0, DB::connection()->transactionLevel()); + $this->assertSame( + 0, + DB::table('testbench_users')->where('email', 'truncation@example.com')->count(), + ); + + DB::table('testbench_users')->insert([ + 'email' => 'truncation@example.com', + 'password' => (string) $lifecycle, + ]); + } + + public static function applicationLifecycles(): array + { + // The second case exercises database state retained by the first lifecycle. + return [ + 'first application' => [1], + 'second application' => [2], + ]; + } +} diff --git a/tests/Testbench/DefaultConfigurationTest.php b/tests/Testbench/DefaultConfigurationTest.php index e9b097db32..2c7e1721f3 100644 --- a/tests/Testbench/DefaultConfigurationTest.php +++ b/tests/Testbench/DefaultConfigurationTest.php @@ -86,9 +86,13 @@ public function itUsesTheCanonicalSqliteMemoryClassification(): void 'driver' => 'sqlite', 'database' => 'file:database?mode=memory&mode=rwc', ]); + $config->set('database.connections.url_memory', [ + 'url' => 'sqlite:///:memory:', + ]); $this->assertTrue($this->usesSqliteInMemoryDatabaseConnection('uri_memory')); $this->assertFalse($this->usesSqliteInMemoryDatabaseConnection('uri_file')); + $this->assertTrue($this->usesSqliteInMemoryDatabaseConnection('url_memory')); } #[Test]