148 lines
6.1 KiB
Python
148 lines
6.1 KiB
Python
from __future__ import annotations
|
|
|
|
import shutil
|
|
import sqlite3
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from game_data_capture.storage.database import MigrationError, StorageDatabase
|
|
|
|
|
|
def _migrations(tmp_path: Path) -> Path:
|
|
source = Path(__file__).parents[3] / "migrations" / "001_initial.sql"
|
|
destination = tmp_path / "migrations"
|
|
destination.mkdir()
|
|
shutil.copy(source, destination / source.name)
|
|
return destination
|
|
|
|
|
|
def test_connection_pragmas_and_private_modes(tmp_path: Path) -> None:
|
|
root = (tmp_path / "utility").resolve()
|
|
database = StorageDatabase(root, _migrations(tmp_path).resolve())
|
|
database.migrate()
|
|
with database.connect() as connection:
|
|
connection.execute("BEGIN IMMEDIATE")
|
|
connection.execute("CREATE TABLE IF NOT EXISTS live_wal(value INTEGER)")
|
|
assert connection.execute("PRAGMA foreign_keys").fetchone()[0] == 1
|
|
assert connection.execute("PRAGMA journal_mode").fetchone()[0] == "wal"
|
|
assert connection.execute("PRAGMA busy_timeout").fetchone()[0] == 5000
|
|
assert connection.execute("PRAGMA synchronous").fetchone()[0] == 2
|
|
for path in (
|
|
database.path.with_name("capture.sqlite3-wal"),
|
|
database.path.with_name("capture.sqlite3-shm"),
|
|
):
|
|
if path.exists():
|
|
assert path.stat().st_mode & 0o077 == 0
|
|
connection.execute("COMMIT")
|
|
assert root.stat().st_mode & 0o777 == 0o700
|
|
assert database.path.stat().st_mode & 0o777 == 0o600
|
|
|
|
|
|
def test_database_symlink_is_rejected(tmp_path: Path) -> None:
|
|
root = (tmp_path / "utility").resolve()
|
|
root.mkdir()
|
|
outside = tmp_path / "outside"
|
|
outside.write_text("x")
|
|
(root / "capture.sqlite3").symlink_to(outside)
|
|
with pytest.raises(MigrationError):
|
|
StorageDatabase(root, _migrations(tmp_path).resolve()).connect()
|
|
|
|
|
|
def test_migration_history_and_failure_rollback(tmp_path: Path) -> None:
|
|
root = (tmp_path / "utility").resolve()
|
|
migrations = _migrations(tmp_path).resolve()
|
|
database = StorageDatabase(root, migrations)
|
|
database.migrate()
|
|
first = migrations / "001_initial.sql"
|
|
first.write_text(first.read_text() + "\n-- changed\n")
|
|
with pytest.raises(MigrationError):
|
|
database.migrate()
|
|
first.write_text((Path(__file__).parents[3] / "migrations" / "001_initial.sql").read_text())
|
|
bad = migrations / "002_bad.sql"
|
|
bad.write_text("CREATE TABLE partial_test(value INTEGER); BAD SQL;")
|
|
with pytest.raises(sqlite3.OperationalError):
|
|
database.migrate()
|
|
with database.connect() as connection:
|
|
assert connection.execute("SELECT count(*) FROM schema_migrations").fetchone()[0] == 1
|
|
assert (
|
|
connection.execute(
|
|
"SELECT name FROM sqlite_master WHERE name='partial_test'"
|
|
).fetchone()
|
|
is None
|
|
)
|
|
bad.write_text("CREATE TABLE partial_test(value INTEGER);")
|
|
database.migrate()
|
|
database.migrate()
|
|
with database.connect() as connection:
|
|
assert connection.execute("SELECT count(*) FROM schema_migrations").fetchone()[0] == 2
|
|
|
|
|
|
def test_migration_removal_rename_and_duplicate_rejected(tmp_path: Path) -> None:
|
|
root = (tmp_path / "utility").resolve()
|
|
migrations = _migrations(tmp_path).resolve()
|
|
database = StorageDatabase(root, migrations)
|
|
database.migrate()
|
|
path = migrations / "001_initial.sql"
|
|
path.rename(migrations / "001_renamed.sql")
|
|
with pytest.raises(MigrationError):
|
|
database.migrate()
|
|
(migrations / "001_renamed.sql").rename(path)
|
|
path.unlink()
|
|
with pytest.raises(MigrationError):
|
|
database.migrate()
|
|
shutil.copy(Path(__file__).parents[3] / "migrations" / "001_initial.sql", path)
|
|
shutil.copy(path, migrations / "001_duplicate.sql")
|
|
with pytest.raises(MigrationError):
|
|
database.migrate()
|
|
|
|
|
|
def test_corpus_migration_preserves_populated_initial_database(tmp_path: Path) -> None:
|
|
root = (tmp_path / "utility").resolve()
|
|
migrations = _migrations(tmp_path).resolve()
|
|
database = StorageDatabase(root, migrations)
|
|
database.migrate()
|
|
with database.connect() as connection:
|
|
connection.execute(
|
|
"INSERT INTO blobs(sha256, byte_size, media_type, created_at) VALUES (?, 1, ?, ?)",
|
|
("0" * 64, "image/png", "time"),
|
|
)
|
|
connection.execute(
|
|
"INSERT INTO captures(capture_id, blob_sha256, state, received_at, receipt_token) "
|
|
"VALUES (?, ?, ?, ?, ?)",
|
|
("capture-0001", "0" * 64, "stored", "time", "receipt-0001"),
|
|
)
|
|
source = Path(__file__).parents[3] / "migrations" / "002_capture_corpus.sql"
|
|
shutil.copy(source, migrations / source.name)
|
|
database.migrate()
|
|
with database.connect() as connection:
|
|
assert connection.execute("SELECT count(*) FROM captures").fetchone()[0] == 1
|
|
assert connection.execute("SELECT count(*) FROM capture_labels").fetchone()[0] == 0
|
|
assert tuple(
|
|
connection.execute(
|
|
"SELECT sha256, byte_size, media_type, created_at FROM blobs"
|
|
).fetchone()
|
|
) == ("0" * 64, 1, "image/png", "time")
|
|
assert tuple(
|
|
connection.execute(
|
|
"SELECT capture_id, blob_sha256, state, received_at, receipt_token, "
|
|
"failure_reason, revision FROM captures"
|
|
).fetchone()
|
|
) == ("capture-0001", "0" * 64, "stored", "time", "receipt-0001", None, 0)
|
|
assert connection.execute("SELECT count(*) FROM schema_migrations").fetchone()[0] == 2
|
|
migration = connection.execute(
|
|
"SELECT filename, checksum FROM schema_migrations WHERE version = 2"
|
|
).fetchone()
|
|
assert migration is not None
|
|
assert migration["filename"] == "002_capture_corpus.sql"
|
|
assert isinstance(migration["checksum"], str) and len(migration["checksum"]) == 64
|
|
names = {row["name"] for row in connection.execute("SELECT name FROM sqlite_master")}
|
|
assert {
|
|
"captures_blob_sha256",
|
|
"jobs_capture_id",
|
|
"ocr_runs_capture_id",
|
|
"runtime_failures_capture_subject",
|
|
"lineage_child_id",
|
|
"lineage_parent_id",
|
|
}.issubset(names)
|