| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- """spoolman_slot_assignments admits the H3C's AMS 3H: ams_id 32 with slots 0-8, externals (255, 2-8).
- Slots 4-8 exist only on the AMS 3H and among an H3C's external inputs, so the
- tray CHECK is ``tray_id >= 0 AND (tray_id <= 3 OR (ams_id IN (32, 255) AND
- tray_id <= 8))``. An upgraded SQLite table is rebuilt to it from upstream's
- CHECKs and from the plain ``tray_id <= 8`` an earlier build of this support
- wrote; a row the latter let through on another unit is dropped.
- """
- from __future__ import annotations
- import pytest
- from sqlalchemy import text
- from sqlalchemy.exc import IntegrityError
- from sqlalchemy.ext.asyncio import create_async_engine
- from backend.app.core.database import _migrate_widen_spoolman_slot_ams_id_range
- from backend.app.models.spoolman_slot_assignment import AMS_ID_CHECK, TRAY_ID_CHECK
- _TABLE = """
- CREATE TABLE spoolman_slot_assignments (
- id INTEGER NOT NULL,
- printer_id INTEGER NOT NULL,
- ams_id INTEGER NOT NULL,
- tray_id INTEGER NOT NULL,
- spoolman_spool_id INTEGER NOT NULL,
- assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
- PRIMARY KEY (id),
- CONSTRAINT uq_slot_assignment UNIQUE (printer_id, ams_id, tray_id),
- CONSTRAINT ck_ams_id_range CHECK ({ams}),
- CONSTRAINT ck_tray_id_range CHECK ({tray})
- )
- """
- _AMS_1274 = "(ams_id >= 0 AND ams_id <= 7) OR (ams_id >= 128 AND ams_id <= 191) OR ams_id = 255"
- # The table as create_all built it for the #1274 model (upstream dev).
- TABLE_1274 = _TABLE.format(ams=_AMS_1274, tray="tray_id >= 0 AND tray_id <= 3")
- # As an earlier build of the H3C support left it: by its own SQLite rebuild...
- TABLE_WIDENED_REBUILT = (
- "CREATE TABLE spoolman_slot_assignments (id INTEGER PRIMARY KEY AUTOINCREMENT, "
- "printer_id INTEGER NOT NULL REFERENCES printers(id) ON DELETE CASCADE, "
- f"ams_id INTEGER NOT NULL CHECK ({AMS_ID_CHECK}), "
- "tray_id INTEGER NOT NULL CHECK (tray_id >= 0 AND tray_id <= 8), "
- "spoolman_spool_id INTEGER NOT NULL, assigned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "
- "CONSTRAINT uq_slot_assignment UNIQUE(printer_id, ams_id, tray_id))"
- )
- # ...and by the live-install script, which rewrote the create_all form in place.
- TABLE_WIDENED_IN_PLACE = _TABLE.format(ams=AMS_ID_CHECK, tray="tray_id >= 0 AND tray_id <= 8")
- TABLE_FINAL = _TABLE.format(ams=AMS_ID_CHECK, tray=TRAY_ID_CHECK)
- @pytest.fixture(autouse=True)
- def force_sqlite_dialect(monkeypatch):
- from backend.app.core import database as database_module, db_dialect
- monkeypatch.setattr(db_dialect, "is_sqlite", lambda: True)
- monkeypatch.setattr(db_dialect, "is_postgres", lambda: False)
- monkeypatch.setattr(database_module, "is_sqlite", lambda: True)
- async def _insert(conn, ams_id: int, tray_id: int, spool: int) -> None:
- await conn.execute(
- text(
- "INSERT INTO spoolman_slot_assignments (printer_id, ams_id, tray_id, spoolman_spool_id) "
- "VALUES (1, :a, :t, :s)"
- ),
- {"a": ams_id, "t": tray_id, "s": spool},
- )
- async def _rows(conn) -> list[tuple]:
- rows = await conn.execute(
- text("SELECT ams_id, tray_id, spoolman_spool_id FROM spoolman_slot_assignments ORDER BY id")
- )
- return [tuple(r) for r in rows.fetchall()]
- async def _assert_final_checks(conn) -> None:
- await _insert(conn, 32, 8, 14) # AMS 3H slot 9
- await _insert(conn, 255, 5, 15) # an H3C external input (250)
- for ams_id, tray_id in ((0, 4), (128, 5), (8, 0), (32, 9), (40, 0), (255, 9)):
- with pytest.raises(IntegrityError):
- async with conn.begin_nested():
- await _insert(conn, ams_id, tray_id, 99)
- indexes = (
- await conn.execute(
- text("SELECT name FROM sqlite_master WHERE type='index' AND tbl_name='spoolman_slot_assignments'")
- )
- ).fetchall()
- assert "ix_slot_assignment_spool" in {r[0] for r in indexes}
- async def _table(conn, ddl: str) -> None:
- await conn.execute(text(ddl))
- await conn.execute(text("CREATE INDEX ix_slot_assignment_spool ON spoolman_slot_assignments (spoolman_spool_id)"))
- @pytest.mark.asyncio
- async def test_upstreams_table_takes_the_ams3h_and_keeps_its_rows():
- engine = create_async_engine("sqlite+aiosqlite:///:memory:")
- async with engine.begin() as conn:
- await _table(conn, TABLE_1274)
- await _insert(conn, 0, 3, 11)
- await _insert(conn, 128, 0, 12)
- await _insert(conn, 255, 1, 13)
- with pytest.raises(IntegrityError):
- async with conn.begin_nested():
- await _insert(conn, 32, 8, 14)
- await _migrate_widen_spoolman_slot_ams_id_range(conn)
- await _migrate_widen_spoolman_slot_ams_id_range(conn) # a second start changes nothing
- assert await _rows(conn) == [(0, 3, 11), (128, 0, 12), (255, 1, 13)]
- await _assert_final_checks(conn)
- await engine.dispose()
- @pytest.mark.asyncio
- @pytest.mark.parametrize("ddl", [TABLE_WIDENED_REBUILT, TABLE_WIDENED_IN_PLACE])
- async def test_a_table_an_earlier_build_widened_is_narrowed_to_real_slots(ddl):
- engine = create_async_engine("sqlite+aiosqlite:///:memory:")
- async with engine.begin() as conn:
- await _table(conn, ddl)
- await _insert(conn, 32, 6, 11)
- await _insert(conn, 255, 4, 12)
- await _insert(conn, 0, 6, 13) # slot 7 of a regular AMS: no such slot
- await _migrate_widen_spoolman_slot_ams_id_range(conn)
- await _migrate_widen_spoolman_slot_ams_id_range(conn)
- assert await _rows(conn) == [(32, 6, 11), (255, 4, 12)]
- await _assert_final_checks(conn)
- await engine.dispose()
- @pytest.mark.asyncio
- async def test_a_final_table_is_left_alone():
- engine = create_async_engine("sqlite+aiosqlite:///:memory:")
- async with engine.begin() as conn:
- await _table(conn, TABLE_FINAL)
- await _insert(conn, 32, 6, 11)
- before = (
- await conn.execute(text("SELECT sql FROM sqlite_master WHERE name='spoolman_slot_assignments'"))
- ).scalar()
- await _migrate_widen_spoolman_slot_ams_id_range(conn)
- after = (
- await conn.execute(text("SELECT sql FROM sqlite_master WHERE name='spoolman_slot_assignments'"))
- ).scalar()
- assert after == before
- assert await _rows(conn) == [(32, 6, 11)]
- await _assert_final_checks(conn)
- await engine.dispose()
- def test_the_model_admits_what_the_migration_does():
- from sqlalchemy import CheckConstraint
- from backend.app.models.spoolman_slot_assignment import SpoolmanSlotAssignment
- checks = {
- c.name: str(c.sqltext) for c in SpoolmanSlotAssignment.__table__.constraints if isinstance(c, CheckConstraint)
- }
- assert checks["ck_ams_id_range"] == AMS_ID_CHECK
- assert (
- checks["ck_tray_id_range"]
- == TRAY_ID_CHECK
- == ("tray_id >= 0 AND (tray_id <= 3 OR (ams_id IN (32, 255) AND tray_id <= 8))")
- )
- def test_the_fresh_install_ddl_matches_the_model():
- import inspect
- import backend.app.core.database as database_module
- source = inspect.getsource(database_module)
- for dialect_marker in ("id INTEGER PRIMARY KEY AUTOINCREMENT,\n printer_id", "id SERIAL PRIMARY KEY,"):
- start = source.index("CREATE TABLE IF NOT EXISTS spoolman_slot_assignments (\n " + dialect_marker)
- ddl = source[start : source.index(')\n """', start)]
- assert f"CHECK ({AMS_ID_CHECK})" in ddl and f"CHECK ({TRAY_ID_CHECK})" in ddl
- def test_the_request_schema_admits_the_ams3h():
- from pydantic import TypeAdapter, ValidationError
- from backend.app.schemas.spoolman import SlotAmsId, SlotTrayId
- assert TypeAdapter(SlotAmsId).validate_python(32) == 32
- assert TypeAdapter(SlotTrayId).validate_python(8) == 8
- for bad_ams in (8, 31, 33):
- with pytest.raises(ValidationError):
- TypeAdapter(SlotAmsId).validate_python(bad_ams)
- with pytest.raises(ValidationError):
- TypeAdapter(SlotTrayId).validate_python(9)
|