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
@@ -0,0 +1,41 @@
|
||||
#!/bin/sh
|
||||
# Run from an isolated checkout on any Docker host. No existing database is touched.
|
||||
set -eu
|
||||
cd "$(dirname "$0")/.."
|
||||
suffix="$$-$(date +%s)"
|
||||
network="meshvault-test-$suffix"
|
||||
database="meshvault-test-db-$suffix"
|
||||
image="meshvault-test:$suffix"
|
||||
runner="meshvault-test-runner-$suffix"
|
||||
cleanup() {
|
||||
docker rm -f "$runner" >/dev/null 2>&1 || true
|
||||
docker rm -f "$database" >/dev/null 2>&1 || true
|
||||
docker network rm "$network" >/dev/null 2>&1 || true
|
||||
docker image rm "$image" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
trap 'exit 130' INT
|
||||
trap 'exit 143' TERM
|
||||
|
||||
docker build -f docker/php/Dockerfile.test -t "$image" .
|
||||
docker network create "$network" >/dev/null
|
||||
# Password is disposable, on a dedicated network, with no published port or persistent volume.
|
||||
docker run -d --rm --name "$database" --network "$network" \
|
||||
-e POSTGRES_USER=meshvault_test -e POSTGRES_DB=meshvault_test \
|
||||
-e POSTGRES_PASSWORD=disposable-test-only postgres:17-bookworm >/dev/null
|
||||
attempt=0
|
||||
until docker exec "$database" pg_isready -h 127.0.0.1 -U meshvault_test -d meshvault_test >/dev/null 2>&1; do
|
||||
attempt=$((attempt + 1))
|
||||
if [ "$attempt" -ge 60 ]; then
|
||||
echo 'PostgreSQL did not become ready' >&2
|
||||
exit 1
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
docker run --rm --name "$runner" --network "$network" \
|
||||
-e MESHVAULT_TEST_PG_DSN="pgsql:host=$database;dbname=meshvault_test" \
|
||||
-e MESHVAULT_TEST_PG_USER=meshvault_test \
|
||||
-e MESHVAULT_TEST_PG_PASSWORD=disposable-test-only \
|
||||
"$image" composer test
|
||||
docker run --rm --name "$runner" "$image" composer lint
|
||||
echo 'PostgreSQL tests and lint completed successfully.'
|
||||
@@ -0,0 +1,11 @@
|
||||
FROM php:8.4-cli-bookworm
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends libonig-dev libxml2-dev libpq-dev libsqlite3-dev unzip \
|
||||
&& docker-php-ext-install mbstring dom pdo_pgsql pdo_sqlite xmlwriter \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
|
||||
WORKDIR /workspace
|
||||
COPY composer.json composer.lock ./
|
||||
RUN composer install --no-interaction --prefer-dist --no-progress
|
||||
COPY . .
|
||||
CMD ["composer", "test"]
|
||||
@@ -0,0 +1,26 @@
|
||||
**
|
||||
!composer.json
|
||||
!composer.lock
|
||||
!src/**
|
||||
!tests/**
|
||||
!db/**
|
||||
!config/**
|
||||
!templates/**
|
||||
!public/index.php
|
||||
!bin/**
|
||||
!dev.sh
|
||||
!dev.ps1
|
||||
!phpunit.xml.dist
|
||||
!phpcs.xml.dist
|
||||
!phinx.php
|
||||
!.editorconfig
|
||||
!docker/**
|
||||
!docker-compose.yml
|
||||
!README.md
|
||||
!CONTRIBUTING.md
|
||||
!LICENSE
|
||||
!assets/**
|
||||
!package.json
|
||||
!package-lock.json
|
||||
!.dockerignore
|
||||
!.gitignore
|
||||
@@ -0,0 +1,47 @@
|
||||
# PostgreSQL integration tests
|
||||
|
||||
From a checkout on a Linux Docker host, run:
|
||||
|
||||
```sh
|
||||
sh bin/test-postgres.sh
|
||||
```
|
||||
|
||||
This builds `docker/php/Dockerfile.test` from the current checkout and lockfile,
|
||||
starts a disposable PostgreSQL 17 container on a unique network, runs the full
|
||||
suite with PostgreSQL enabled, then runs lint. It publishes no ports, mounts no
|
||||
existing database volumes, and removes its database container, network and test
|
||||
image on exit. Build layers remain cached. PHP 8.4 is used for the PHPUnit 12 test
|
||||
runtime; the production image is unchanged. Docker and network access for the
|
||||
initial image/dependency build are required.
|
||||
|
||||
For remote validation, copy the current source to a **separate** remote directory
|
||||
(do not overwrite an existing dirty checkout), then execute the command there.
|
||||
Do not copy `.env`, host `vendor`, collection directories or other secrets.
|
||||
Capture the command exit status along with its output.
|
||||
|
||||
## Existing dedicated test database
|
||||
|
||||
Alternatively, export these variables before invoking `composer test`:
|
||||
|
||||
```sh
|
||||
export MESHVAULT_TEST_PG_DSN='pgsql:host=127.0.0.1;port=5432;dbname=meshvault_test'
|
||||
export MESHVAULT_TEST_PG_USER=meshvault_test
|
||||
# Supply MESHVAULT_TEST_PG_PASSWORD securely through your environment.
|
||||
composer test
|
||||
```
|
||||
|
||||
Use a dedicated test database and a role permitted to create schemas. Each test
|
||||
migrates production Phinx migrations into a randomly named schema, sets its
|
||||
connections' search path to that schema only, and drops that schema on teardown.
|
||||
Configured connection or migration errors fail the suite rather than skip it.
|
||||
An abruptly killed process may leave `meshvault_test_*` schemas; only remove
|
||||
schemas known to belong to a finished test run.
|
||||
|
||||
Without `MESHVAULT_TEST_PG_DSN`, repository and pipeline tests continue to use
|
||||
SQLite and the five PostgreSQL-only tests skip. Those tests cover schema isolation,
|
||||
overlapping claims using `SKIP LOCKED`, rollback/reclaim, and item-lock timeout and
|
||||
retry using two independent PDO sessions. They are not a multiprocess load test.
|
||||
|
||||
The older `MigrationTest` suite uses the separate application `MESHVAULT_DB_*`
|
||||
variables and remains skipped in this isolated runner. Production migrations are
|
||||
nevertheless exercised by the repository/pipeline fixtures on PostgreSQL.
|
||||
@@ -185,14 +185,33 @@
|
||||
- Live CLI exit-code probe: `help`=0, `bogus`=1, no args=1, `import` (no path)=1,
|
||||
`worker --sleep=x`=1, `worker-once extra`=1, `import C:\nope` (unconfigured env)=2.
|
||||
|
||||
### PostgreSQL follow-up validation (2026-10-12)
|
||||
|
||||
- Windows SQLite/default run: **291 tests, 1801 assertions, 22 skips**, test exit 0;
|
||||
lint exit 0.
|
||||
- Remote isolated workspace `/tmp/meshvault-pg-tests-20261011` on `192.168.2.23`:
|
||||
PHP 8.4.26, PostgreSQL 17, **291 tests, 1863 assertions, 7 skips**, runner exit 0
|
||||
after both tests and lint. Log: `/tmp/meshvault-pg-run.log`.
|
||||
- Existing `/tmp/MeshVault` checkout and dev database were not modified. Disposable
|
||||
database/test containers and network were removed after the run.
|
||||
- Linux execution uncovered an invalid canonical-root fixture: it supplied a symlink root,
|
||||
intentionally rejected by PathSafety. It now uses a safe noncanonical trailing separator
|
||||
and runs on Windows too. No production safety policy was weakened.
|
||||
- New test tooling and follow-up changes are included in the PostgreSQL validation follow-up
|
||||
commit; the original M3 work is in `9d277d9`.
|
||||
|
||||
### Limitations
|
||||
|
||||
- **PostgreSQL was not exercised.** Integration tests run against SQLite built from the Phinx
|
||||
migrations; `FOR UPDATE SKIP LOCKED` is driver-detected but unverified on a live server.
|
||||
`tests/Integration/MigrationTest.php` still skips without DB credentials.
|
||||
- **PostgreSQL exercised on 2026-10-12.** The isolated remote Docker runner uses PostgreSQL 17
|
||||
and production Phinx migrations for the repository and import pipeline suites. Two-session
|
||||
tests verify overlapping `SKIP LOCKED` claims, rollback/reclaim, item lock timeout/retry,
|
||||
explicit `FOR UPDATE` read locking and schema isolation. This is not a multiprocess load test.
|
||||
The older `MigrationTest` still skips without application DB credentials; the runner does
|
||||
not point it at the existing dev database. See `docs/postgres-tests.md`.
|
||||
- **Signals are POSIX-only.** `bin/meshvault` installs `SIGTERM`/`SIGINT` handlers when `pcntl`
|
||||
is available; on Windows the worker stops only at `--max-iterations`.
|
||||
- Graceful stop is verified between items, not mid-copy.
|
||||
- Symlink unit tests skip on hosts that cannot create links (11 skips here).
|
||||
- Nothing is committed, merged or pushed; all work is uncommitted in the working tree.
|
||||
- Original M3 work was committed as `9d277d9`; PostgreSQL validation is recorded in the
|
||||
subsequent follow-up commit.
|
||||
|
||||
@@ -42,9 +42,13 @@ final class ImportPipelineTest extends TestCase
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($this->pdo, $this->jobs, $this->catalog);
|
||||
@unlink($this->database);
|
||||
RepositoryDatabase::remove($this->library);
|
||||
RepositoryDatabase::remove($this->source);
|
||||
RepositoryDatabase::cleanup($this->database);
|
||||
if (isset($this->library)) {
|
||||
RepositoryDatabase::remove($this->library);
|
||||
}
|
||||
if (isset($this->source)) {
|
||||
RepositoryDatabase::remove($this->source);
|
||||
}
|
||||
}
|
||||
|
||||
private function service(?SpaceChecker $space = null): ImportService
|
||||
@@ -374,22 +378,15 @@ final class ImportPipelineTest extends TestCase
|
||||
|
||||
public function testWorkerUsesCanonicalLibraryRootForRelativePaths(): void
|
||||
{
|
||||
// Test that the worker uses canonical (realpath) library root for
|
||||
// computing relative paths, not the raw string which might have
|
||||
// symlinks or case differences.
|
||||
//
|
||||
// Create library with a symlink in the path
|
||||
$realLibrary = $this->library;
|
||||
$linkLibrary = $realLibrary . '-link';
|
||||
if (!@symlink($realLibrary, $linkLibrary)) {
|
||||
self::markTestSkipped('Host cannot create symlinks.');
|
||||
}
|
||||
// A trailing separator is safe but differs from the canonical root.
|
||||
// Symlink roots are deliberately rejected by PathSafety.
|
||||
$rawLibrary = $this->library . DIRECTORY_SEPARATOR;
|
||||
|
||||
$this->writeStl('planes/wing.stl');
|
||||
$job = $this->service()->import($this->source);
|
||||
|
||||
// Create worker with symlinked library root
|
||||
$safety = new PathSafety($linkLibrary);
|
||||
$safety = new PathSafety($rawLibrary);
|
||||
self::assertNotSame($rawLibrary, $safety->libraryRoot());
|
||||
$sourceSafety = new PathSafety($this->source);
|
||||
$worker = new ImportWorker(
|
||||
$this->jobs,
|
||||
@@ -399,20 +396,17 @@ final class ImportPipelineTest extends TestCase
|
||||
new StlValidator($sourceSafety),
|
||||
$safety,
|
||||
new SpaceChecker(),
|
||||
$linkLibrary, // Symlinked path
|
||||
$rawLibrary,
|
||||
new NullLogger(),
|
||||
);
|
||||
|
||||
$worker->runOnce();
|
||||
$item = $this->jobs->items($job)[0];
|
||||
|
||||
// The managed_path should be relative to the CANONICAL root, not the symlink
|
||||
self::assertStringNotContainsString('link', $item['managed_path']);
|
||||
self::assertSame('Completed', $item['status']);
|
||||
self::assertStringStartsWith('planes/', $item['managed_path']);
|
||||
$published = $this->library . DIRECTORY_SEPARATOR
|
||||
. str_replace('/', DIRECTORY_SEPARATOR, $item['managed_path']);
|
||||
self::assertFileExists($published);
|
||||
|
||||
// Cleanup
|
||||
unlink($linkLibrary);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MeshVault\Tests\Integration;
|
||||
|
||||
use MeshVault\Repository\PdoImportJobRepository;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/** Opt in with MESHVAULT_TEST_PG_DSN (and optional PG_USER / PG_PASSWORD with the same prefix). */
|
||||
final class PostgresRepositoryTest extends TestCase
|
||||
{
|
||||
private string $database;
|
||||
private PDO $pdo;
|
||||
private PDO $other;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
if (!RepositoryDatabase::postgresConfigured()) {
|
||||
self::markTestSkipped('Set MESHVAULT_TEST_PG_DSN to run PostgreSQL concurrency tests.');
|
||||
}
|
||||
// Configured connection/migration failures must be errors, never skips.
|
||||
$this->database = tempnam(sys_get_temp_dir(), 'meshvault-pg-');
|
||||
$this->pdo = RepositoryDatabase::migrate($this->database);
|
||||
$this->other = RepositoryDatabase::connect($this->database);
|
||||
foreach ([$this->pdo, $this->other] as $pdo) {
|
||||
$pdo->exec("SET lock_timeout = '250ms'");
|
||||
$pdo->exec("SET statement_timeout = '5s'");
|
||||
}
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
foreach (['pdo', 'other'] as $property) {
|
||||
if (isset($this->$property) && $this->$property->inTransaction()) {
|
||||
$this->$property->rollBack();
|
||||
}
|
||||
unset($this->$property);
|
||||
}
|
||||
if (isset($this->database)) {
|
||||
RepositoryDatabase::cleanup($this->database);
|
||||
}
|
||||
}
|
||||
|
||||
public function testMigrationsAndCleanupAreIsolatedToTheGeneratedSchema(): void
|
||||
{
|
||||
$path = tempnam(sys_get_temp_dir(), 'meshvault-pg-isolated-');
|
||||
try {
|
||||
$isolated = RepositoryDatabase::migrate($path);
|
||||
$schema = $isolated->query('SELECT current_schema()')->fetchColumn();
|
||||
$original = $this->pdo->query('SELECT current_schema()')->fetchColumn();
|
||||
self::assertMatchesRegularExpression('/^meshvault_test_[a-f0-9]{32}$/', $schema);
|
||||
self::assertNotSame($original, $schema);
|
||||
self::assertSame($original, $this->other->query('SELECT current_schema()')->fetchColumn());
|
||||
self::assertNotSame(
|
||||
$this->pdo->query('SELECT pg_backend_pid()')->fetchColumn(),
|
||||
$this->other->query('SELECT pg_backend_pid()')->fetchColumn(),
|
||||
);
|
||||
$jobs = new PdoImportJobRepository($this->pdo);
|
||||
$job = $jobs->create('/survives-cleanup', []);
|
||||
self::assertSame(0, (int) $isolated->query('SELECT COUNT(*) FROM import_jobs')->fetchColumn());
|
||||
unset($isolated);
|
||||
RepositoryDatabase::cleanup($path);
|
||||
$exists = $this->pdo->prepare('SELECT COUNT(*) FROM pg_namespace WHERE nspname = ?');
|
||||
$exists->execute([$schema]);
|
||||
self::assertSame(0, (int) $exists->fetchColumn());
|
||||
self::assertSame('/survives-cleanup', $jobs->get($job)['source_path']);
|
||||
// An unregistered path is never interpreted as a schema identifier.
|
||||
RepositoryDatabase::cleanup($schema);
|
||||
} finally {
|
||||
unset($isolated);
|
||||
RepositoryDatabase::cleanup($path);
|
||||
}
|
||||
}
|
||||
|
||||
public function testOverlappingClaimsSkipLockedJobsAndReturnNullWhenAllAreLocked(): void
|
||||
{
|
||||
$jobs = new PdoImportJobRepository($this->pdo);
|
||||
$other = new PdoImportJobRepository($this->other);
|
||||
$first = $jobs->create('/first', []);
|
||||
$second = $jobs->create('/second', []);
|
||||
|
||||
// The outer transaction must retain claimNext's row lock after it returns.
|
||||
$this->pdo->beginTransaction();
|
||||
self::assertSame($first, (int) $jobs->claimNext()['id']);
|
||||
self::assertTrue($this->pdo->inTransaction());
|
||||
self::assertSame('Pending', $other->get($first)['status']);
|
||||
self::assertSame($second, (int) $other->claimNext()['id']);
|
||||
self::assertNull($other->claimNext());
|
||||
self::assertTrue($this->pdo->inTransaction());
|
||||
$this->pdo->commit();
|
||||
self::assertSame('Copying', $other->get($first)['status']);
|
||||
self::assertSame('Copying', $other->get($second)['status']);
|
||||
}
|
||||
|
||||
public function testRolledBackClaimCanBeReclaimedByTheOtherConnection(): void
|
||||
{
|
||||
$jobs = new PdoImportJobRepository($this->pdo);
|
||||
$other = new PdoImportJobRepository($this->other);
|
||||
$job = $jobs->create('/rollback', []);
|
||||
$this->pdo->beginTransaction();
|
||||
self::assertSame($job, (int) $jobs->claimNext()['id']);
|
||||
self::assertNull($other->claimNext());
|
||||
$this->pdo->rollBack();
|
||||
self::assertSame('Pending', $other->get($job)['status']);
|
||||
self::assertNull($other->get($job)['started_at']);
|
||||
self::assertSame($job, (int) $other->claimNext()['id']);
|
||||
self::assertNull($jobs->claimNext());
|
||||
}
|
||||
|
||||
public function testTerminalNoOpStillAcquiresExplicitItemReadLock(): void
|
||||
{
|
||||
$jobs = new PdoImportJobRepository($this->pdo);
|
||||
$other = new PdoImportJobRepository($this->other);
|
||||
$job = $jobs->create('/terminal', [['source_path' => '/terminal/a', 'size' => 7]]);
|
||||
$item = (int) $jobs->items($job)[0]['id'];
|
||||
$jobs->finishItem($item, 'Completed');
|
||||
$this->pdo->beginTransaction();
|
||||
$statement = $this->pdo->prepare('SELECT id FROM import_items WHERE id = ? FOR SHARE');
|
||||
$statement->execute([$item]);
|
||||
$statement->closeCursor();
|
||||
try {
|
||||
// Already terminal: no UPDATE occurs, so only SELECT FOR UPDATE can block.
|
||||
$other->finishItem($item, 'Completed');
|
||||
self::fail('The explicit item read lock must conflict with FOR SHARE.');
|
||||
} catch (PDOException $error) {
|
||||
self::assertSame('55P03', $error->getCode());
|
||||
}
|
||||
self::assertFalse($this->other->inTransaction());
|
||||
$this->pdo->commit();
|
||||
$other->finishItem($item, 'Completed');
|
||||
self::assertSame(1, (int) $jobs->get($job)['processed_files']);
|
||||
}
|
||||
|
||||
public function testItemLockTimesOutWithoutPublishingProgressAndCanBeRetried(): void
|
||||
{
|
||||
$jobs = new PdoImportJobRepository($this->pdo);
|
||||
$other = new PdoImportJobRepository($this->other);
|
||||
$job = $jobs->create('/item', [['source_path' => '/item/a', 'size' => 7]]);
|
||||
$jobs->claimNext();
|
||||
$item = (int) $jobs->items($job)[0]['id'];
|
||||
$this->pdo->beginTransaction();
|
||||
$jobs->planItem($item, 'a.stl', str_repeat('a', 64), 7);
|
||||
self::assertTrue($this->pdo->inTransaction());
|
||||
try {
|
||||
$other->finishItem($item, 'Completed');
|
||||
self::fail('A concurrent item transition must wait for the item lock.');
|
||||
} catch (PDOException $error) {
|
||||
self::assertSame('55P03', $error->getCode(), 'Expected PostgreSQL lock_not_available.');
|
||||
}
|
||||
self::assertFalse($this->other->inTransaction());
|
||||
self::assertSame('Pending', $other->items($job)[0]['status']);
|
||||
self::assertSame(0, (int) $other->get($job)['processed_files']);
|
||||
$this->pdo->commit();
|
||||
$other->finishItem($item, 'Completed');
|
||||
self::assertSame('Completed', $jobs->items($job)[0]['status']);
|
||||
self::assertSame(1, (int) $jobs->get($job)['processed_files']);
|
||||
self::assertSame(7, (int) $jobs->get($job)['copied_bytes']);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -27,7 +27,15 @@ final class RepositoryTest extends TestCase
|
||||
protected function tearDown(): void
|
||||
{
|
||||
unset($this->pdo);
|
||||
unlink($this->database);
|
||||
RepositoryDatabase::cleanup($this->database);
|
||||
}
|
||||
|
||||
public function testSecondConnectionUsesTheMigratedDatabase(): void
|
||||
{
|
||||
$jobs = new PdoImportJobRepository($this->pdo);
|
||||
$job = $jobs->create('/shared', []);
|
||||
$other = new PdoImportJobRepository(RepositoryDatabase::connect($this->database));
|
||||
self::assertSame('/shared', $other->get($job)['source_path']);
|
||||
}
|
||||
|
||||
public function testCatalogGroupingAndFileLifecycle(): void
|
||||
@@ -126,7 +134,7 @@ final class RepositoryTest extends TestCase
|
||||
$catalog = new PdoCatalogRepository($this->pdo);
|
||||
$job = $jobs->create('/', [['source_path' => '/a', 'size' => 5, 'model_id' => null]]);
|
||||
$second = $jobs->create('/', []);
|
||||
$other = new PdoImportJobRepository(new PDO('sqlite:' . $this->database));
|
||||
$other = new PdoImportJobRepository(RepositoryDatabase::connect($this->database));
|
||||
self::assertSame($job, (int) $jobs->claimNext()['id']);
|
||||
self::assertSame($second, (int) $other->claimNext()['id']);
|
||||
self::assertNull($other->claimNext());
|
||||
|
||||
Reference in new issue
Block a user