Files
shd.build/tools/game-data-capture/tests/integration/storage/test_database.py
T

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)