Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/todo.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
20 changes: 20 additions & 0 deletions src/database/src/SQLiteDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Hypervel\Database;

use InvalidArgumentException;

class SQLiteDatabase
{
/**
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
82 changes: 82 additions & 0 deletions src/foundation/src/Testing/DatabaseTruncation.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -24,6 +31,9 @@ trait DatabaseTruncation
*/
protected function truncateDatabaseTables(): void
{
$this->ensureParallelDatabaseExists();
$this->restoreInMemoryDatabases();

$this->beforeTruncatingDatabase();

// Migrate and seed the database on first run...
Expand All @@ -32,6 +42,8 @@ protected function truncateDatabaseTables(): void

$this->app->make(Kernel::class)->setArtisan(null);

$this->cacheInMemoryDatabases();

RefreshDatabaseState::$migrated = true;

return;
Expand All @@ -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.
*/
Expand Down
7 changes: 3 additions & 4 deletions src/foundation/src/Testing/RefreshDatabase.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
10 changes: 3 additions & 7 deletions src/testbench/src/Concerns/HandlesDatabases.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
68 changes: 52 additions & 16 deletions src/testbench/src/Concerns/InteractsWithMigrations.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
});
Expand All @@ -41,14 +42,21 @@ 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;

try {
if (
(count($processors) > 0 && static::usesRefreshDatabaseTestingConcern())
|| ($hasInMemoryConnections && $this->usesSqliteInMemoryDatabaseConnection())
|| (
! $preservesInMemoryDatabase
&& $hasInMemoryConnections
&& $this->usesInMemoryDatabaseForMigrationState()
)
) {
ResetRefreshDatabaseState::run();
}
Expand Down Expand Up @@ -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>|string $paths */
load_migration_paths($app, $paths);

return;
}

/** @var array<string, mixed>|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()
&& (
Expand All @@ -92,19 +128,6 @@ protected function loadMigrationsFrom(array|string $paths): void
)
)
);

if (
(is_string($paths) || Arr::isList($paths))
&& $refreshesDatabase
) {
/** @var list<string>|string $paths */
load_migration_paths($app, $paths);

return;
}

/** @var array<string, mixed>|string $paths */
$this->runMigrationProcessor($app, $this->resolvePackageMigrationsOptions($paths));
}

/**
Expand Down Expand Up @@ -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);
Expand Down
19 changes: 1 addition & 18 deletions src/testbench/src/Concerns/WithHypervelMigrations.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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());
});
Expand Down
Loading