test: validate M3 imports and locking on PostgreSQL
Add isolated PostgreSQL fixtures, two-session locking coverage, and a disposable remote Docker test runner. Correct the canonical-root fixture and document verified SQLite and PostgreSQL results.
This commit is contained in:
1 parent
9d277d9e72
commit
c1ef2ee97a
9 files changed
+420
-37
No files matched your search
@@ -10,27 +10,102 @@ use Phinx\Migration\Manager;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
|
||||
/** Runs the production migrations against SQLite and manages scratch directories. */
|
||||
/** Runs production migrations against SQLite, or an explicitly configured PostgreSQL database. */
|
||||
final class RepositoryDatabase
|
||||
{
|
||||
/** @var array<string, array{schema: string, connection: PDO}> */
|
||||
private static array $schemas = [];
|
||||
|
||||
public static function postgresConfigured(): bool
|
||||
{
|
||||
$dsn = getenv('MESHVAULT_TEST_PG_DSN');
|
||||
return $dsn !== false && $dsn !== '';
|
||||
}
|
||||
|
||||
/** Never a hand-maintained test schema: drift must fail the suite. */
|
||||
public static function migrate(string $path): PDO
|
||||
{
|
||||
if (isset(self::$schemas[$path])) {
|
||||
throw new \LogicException('Clean up the previous database before migrating this path again.');
|
||||
}
|
||||
$environment = ['adapter' => 'sqlite'];
|
||||
if (self::postgresConfigured()) {
|
||||
$pdo = self::postgresConnection();
|
||||
$schema = 'meshvault_test_' . bin2hex(random_bytes(16));
|
||||
$pdo->exec('CREATE SCHEMA "' . $schema . '"');
|
||||
self::$schemas[$path] = ['schema' => $schema, 'connection' => $pdo];
|
||||
$pdo->exec('SET search_path TO "' . $schema . '"');
|
||||
$environment = ['adapter' => 'pgsql', 'schema' => $schema];
|
||||
} else {
|
||||
$pdo = self::connect($path);
|
||||
}
|
||||
try {
|
||||
$config = new Config([
|
||||
'paths' => ['migrations' => dirname(__DIR__, 2) . '/db/migrations'],
|
||||
'environments' => [
|
||||
'default_migration_table' => 'phinxlog',
|
||||
'default_database' => 'test',
|
||||
'test' => $environment + ['connection' => $pdo],
|
||||
],
|
||||
]);
|
||||
(new Manager($config, new ArrayInput([]), new NullOutput()))->migrate('test');
|
||||
if (isset(self::$schemas[$path])) {
|
||||
// Phinx may add public to search_path; repository queries must remain isolated.
|
||||
$pdo->exec('SET search_path TO "' . self::$schemas[$path]['schema'] . '"');
|
||||
}
|
||||
return $pdo;
|
||||
} catch (\Throwable $error) {
|
||||
self::cleanup($path);
|
||||
throw $error;
|
||||
}
|
||||
}
|
||||
|
||||
public static function connect(string $path): PDO
|
||||
{
|
||||
if (isset(self::$schemas[$path])) {
|
||||
$pdo = self::postgresConnection();
|
||||
$pdo->exec('SET search_path TO "' . self::$schemas[$path]['schema'] . '"');
|
||||
return $pdo;
|
||||
}
|
||||
if (self::postgresConfigured()) {
|
||||
throw new \LogicException('Migrate this path before connecting to PostgreSQL.');
|
||||
}
|
||||
$pdo = new PDO('sqlite:' . $path, null, null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
||||
$config = new Config([
|
||||
'paths' => ['migrations' => dirname(__DIR__, 2) . '/db/migrations'],
|
||||
'environments' => [
|
||||
'default_migration_table' => 'phinxlog',
|
||||
'default_database' => 'test',
|
||||
'test' => ['adapter' => 'sqlite', 'connection' => $pdo],
|
||||
],
|
||||
]);
|
||||
(new Manager($config, new ArrayInput([]), new NullOutput()))->migrate('test');
|
||||
$pdo->exec('PRAGMA foreign_keys = ON');
|
||||
$pdo->exec('PRAGMA busy_timeout = 5000');
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
public static function cleanup(string $path): void
|
||||
{
|
||||
if (isset(self::$schemas[$path])) {
|
||||
['schema' => $schema, 'connection' => $pdo] = self::$schemas[$path];
|
||||
if ($pdo->inTransaction()) {
|
||||
$pdo->rollBack();
|
||||
}
|
||||
// Only identifiers generated and registered by migrate() can reach this statement.
|
||||
$pdo->exec('DROP SCHEMA "' . $schema . '" CASCADE');
|
||||
unset(self::$schemas[$path]);
|
||||
}
|
||||
if (is_file($path)) {
|
||||
unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
private static function postgresConnection(): PDO
|
||||
{
|
||||
$dsn = (string) getenv('MESHVAULT_TEST_PG_DSN');
|
||||
if (!str_starts_with($dsn, 'pgsql:')) {
|
||||
throw new \InvalidArgumentException('MESHVAULT_TEST_PG_DSN must be a pgsql: DSN.');
|
||||
}
|
||||
return new PDO(
|
||||
$dsn,
|
||||
($user = getenv('MESHVAULT_TEST_PG_USER')) === false ? null : $user,
|
||||
($password = getenv('MESHVAULT_TEST_PG_PASSWORD')) === false ? null : $password,
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION],
|
||||
);
|
||||
}
|
||||
|
||||
public static function directory(string $prefix): string
|
||||
{
|
||||
$path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'meshvault-' . $prefix . '-' . bin2hex(random_bytes(6));
|
||||
|
||||
Reference in new issue
Block a user