@@ -0,0 +1,129 @@
|
||||
"""
|
||||
После Alembic upgrade (ревизия wesp_2_0) SQLite содержит все колонки ORM: recipes и reports.
|
||||
|
||||
Проверка живой развёртки (без upgrade):
|
||||
|
||||
WESP_SCHEMA_CHECK_LIVE_DB=1 \\
|
||||
WESP_RECIPES_DB_URI=sqlite:////path/to/recipes.db \\
|
||||
WESP_REPORTS_DB_URI=sqlite:////path/to/reports.db \\
|
||||
python3 -m pytest tests/test_migrated_schema_matches_models.py -q
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import inspect as sa_inspect
|
||||
|
||||
from app import create_app, db
|
||||
from config import Config
|
||||
|
||||
|
||||
def _iter_main_bind_models():
|
||||
import app.models # noqa: F401 — регистрация мапперов
|
||||
|
||||
for mapper in db.Model.registry.mappers:
|
||||
cls = mapper.class_
|
||||
if getattr(cls, "__bind_key__", None) == "reports":
|
||||
continue
|
||||
if cls.__dict__.get("__abstract__", False):
|
||||
continue
|
||||
tab = getattr(cls, "__tablename__", None)
|
||||
if not tab:
|
||||
continue
|
||||
yield cls
|
||||
|
||||
|
||||
def _iter_reports_bind_models():
|
||||
import app.models # noqa: F401
|
||||
|
||||
for mapper in db.Model.registry.mappers:
|
||||
cls = mapper.class_
|
||||
if getattr(cls, "__bind_key__", None) != "reports":
|
||||
continue
|
||||
if cls.__dict__.get("__abstract__", False):
|
||||
continue
|
||||
tab = getattr(cls, "__tablename__", None)
|
||||
if not tab:
|
||||
continue
|
||||
yield cls
|
||||
|
||||
|
||||
def _assert_columns_match(engine, model_iter, label: str) -> None:
|
||||
insp = sa_inspect(engine)
|
||||
missing: list[str] = []
|
||||
for cls in model_iter:
|
||||
tab = cls.__tablename__
|
||||
if not insp.has_table(tab):
|
||||
missing.append(f"{cls.__name__}: table {tab!r} missing")
|
||||
continue
|
||||
actual = {c["name"] for c in insp.get_columns(tab)}
|
||||
for col in cls.__table__.columns:
|
||||
if col.key not in actual:
|
||||
missing.append(f"{cls.__name__}.{col.key}: column missing in {tab}")
|
||||
assert not missing, f"{label}:\n" + "\n".join(missing)
|
||||
|
||||
|
||||
class MigratedSchemaMatchesModelsTest(unittest.TestCase):
|
||||
"""Свежие файлы SQLite + полный upgrade head."""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self._tmp = tempfile.mkdtemp(prefix="wesp-schema-migration-")
|
||||
recipes = Path(self._tmp) / "recipes.db"
|
||||
reports = Path(self._tmp) / "reports.db"
|
||||
|
||||
class C(Config):
|
||||
TESTING = False
|
||||
WESP_BACKGROUND_STARTUP = False
|
||||
SQLALCHEMY_DATABASE_URI = f"sqlite:///{recipes}"
|
||||
SQLALCHEMY_BINDS = {"reports": f"sqlite:///{reports}"}
|
||||
SECRET_KEY = "schema-test-secret"
|
||||
SYNC_BACKGROUND_REQUEUE = False
|
||||
SYNC_CLIENT_AUTOSTART = False
|
||||
WESP_ADMIN_LOG_PATH = ""
|
||||
WESP_ADMIN_HARDWARE_LOG_PATH = ""
|
||||
WESP_ADMIN_UI_ACTIVITY_LOG_PATH = ""
|
||||
|
||||
self.app = create_app(C, run_migrations=True)
|
||||
self._ctx = self.app.app_context()
|
||||
self._ctx.push()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self._ctx.pop()
|
||||
|
||||
def test_recipes_db_columns_cover_all_main_models(self) -> None:
|
||||
_assert_columns_match(db.engine, _iter_main_bind_models(), "recipes.db / main bind")
|
||||
|
||||
def test_reports_db_columns_cover_reports_models(self) -> None:
|
||||
engine = db.engines["reports"]
|
||||
_assert_columns_match(engine, _iter_reports_bind_models(), "reports.db")
|
||||
|
||||
|
||||
@unittest.skipUnless(
|
||||
os.getenv("WESP_SCHEMA_CHECK_LIVE_DB", "").strip() in {"1", "true", "yes"},
|
||||
"Задайте WESP_SCHEMA_CHECK_LIVE_DB=1 и WESP_RECIPES_DB_URI для проверки живой БД",
|
||||
)
|
||||
class LiveRecipesDbSchemaTest(unittest.TestCase):
|
||||
"""Опционально: проверка существующих файлов БД (без upgrade)."""
|
||||
|
||||
def test_live_db_columns_cover_models(self) -> None:
|
||||
uri = (os.getenv("WESP_RECIPES_DB_URI") or "").strip()
|
||||
self.assertTrue(uri, "Нужен WESP_RECIPES_DB_URI для живой проверки")
|
||||
reports_uri = (os.getenv("WESP_REPORTS_DB_URI") or "").strip()
|
||||
|
||||
class C(Config):
|
||||
SQLALCHEMY_DATABASE_URI = uri
|
||||
SQLALCHEMY_BINDS = {"reports": reports_uri} if reports_uri else {}
|
||||
TESTING = True
|
||||
SYNC_BACKGROUND_REQUEUE = False
|
||||
WESP_ADMIN_LOG_PATH = ""
|
||||
|
||||
app = create_app(C, run_migrations=False)
|
||||
with app.app_context():
|
||||
_assert_columns_match(db.engine, _iter_main_bind_models(), "живая recipes.db")
|
||||
if reports_uri:
|
||||
eng = db.engines["reports"]
|
||||
_assert_columns_match(eng, _iter_reports_bind_models(), "живая reports.db")
|
||||
Reference in New Issue
Block a user