test_ams_slot_location_cleanup.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. """Cleanup of AMS slot markers imported into the storage-location catalogue.
  2. Bambuddy used to write the slot a spool was loaded into -- "<printer> - AMS A1"
  3. -- into Spoolman's ``location`` field, and the location sync then imported every
  4. distinct one as a storage location. ``_migrate_drop_ams_slot_locations`` clears
  5. the rows that already landed; the import side is covered in
  6. ``test_location_service.py``.
  7. """
  8. import pytest
  9. from sqlalchemy import text
  10. from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
  11. import backend.app.models # noqa: F401 - populate Base.metadata
  12. from backend.app.core.database import Base, _migrate_drop_ams_slot_locations
  13. from backend.app.models.location import Location
  14. from backend.app.models.spool import Spool
  15. from backend.app.services.location_service import assign_location_name
  16. FLAG = "_cleanup_ams_slot_locations_done"
  17. @pytest.fixture
  18. async def engine(tmp_path):
  19. eng = create_async_engine(f"sqlite+aiosqlite:///{tmp_path}/t.db")
  20. async with eng.begin() as conn:
  21. await conn.run_sync(Base.metadata.create_all)
  22. try:
  23. yield eng
  24. finally:
  25. await eng.dispose()
  26. def _location(name: str) -> Location:
  27. loc = Location()
  28. assign_location_name(loc, name)
  29. return loc
  30. async def _names(db) -> set[str]:
  31. return {r[0] for r in (await db.execute(text("SELECT name FROM locations"))).fetchall()}
  32. async def _run(engine):
  33. async with engine.begin() as conn:
  34. await _migrate_drop_ams_slot_locations(conn)
  35. @pytest.mark.asyncio
  36. async def test_removes_the_slot_markers_and_keeps_real_locations(engine):
  37. sm = async_sessionmaker(engine, expire_on_commit=False)
  38. async with sm() as db:
  39. db.add_all(
  40. [
  41. _location("H2D-1 - AMS A1"),
  42. _location("H2D-1 - AMS C3"),
  43. _location("X1C-2 - AMS-HT A1"),
  44. _location("P1S - External Spool"),
  45. _location("Drybox 1"),
  46. _location("Shelf A"),
  47. ]
  48. )
  49. await db.commit()
  50. await _run(engine)
  51. async with sm() as db:
  52. assert await _names(db) == {"Drybox 1", "Shelf A"}
  53. @pytest.mark.asyncio
  54. async def test_keeps_a_marker_a_spool_is_actually_filed_under(engine):
  55. """Deleting it would strand the spool's location, and someone who has
  56. deliberately filed spools under that name meant it."""
  57. sm = async_sessionmaker(engine, expire_on_commit=False)
  58. async with sm() as db:
  59. loc = _location("H2D-1 - AMS A1")
  60. db.add(loc)
  61. await db.flush()
  62. db.add(
  63. Spool(
  64. material="PLA",
  65. label_weight=1000,
  66. location_id=loc.id,
  67. storage_location="H2D-1 - AMS A1",
  68. )
  69. )
  70. await db.commit()
  71. await _run(engine)
  72. async with sm() as db:
  73. assert await _names(db) == {"H2D-1 - AMS A1"}
  74. @pytest.mark.asyncio
  75. async def test_keeps_a_marker_a_legacy_free_text_spool_still_names(engine):
  76. """Rows predating the location catalogue carry the name without the FK, and
  77. the rename cascade still matches them on it."""
  78. sm = async_sessionmaker(engine, expire_on_commit=False)
  79. async with sm() as db:
  80. db.add(_location("H2D-1 - AMS A1"))
  81. await db.flush()
  82. # Whitespace and case around the name are the legacy shape the rename
  83. # cascade already has to cope with, so the guard has to match it too.
  84. db.add(Spool(material="PLA", label_weight=1000, storage_location=" h2d-1 - ams a1 "))
  85. await db.commit()
  86. await _run(engine)
  87. async with sm() as db:
  88. assert await _names(db) == {"H2D-1 - AMS A1"}
  89. @pytest.mark.asyncio
  90. async def test_runs_exactly_once(engine):
  91. """A location the user creates afterwards is theirs, whatever it is named."""
  92. sm = async_sessionmaker(engine, expire_on_commit=False)
  93. async with sm() as db:
  94. db.add(_location("H2D-1 - AMS A1"))
  95. await db.commit()
  96. await _run(engine)
  97. async with sm() as db:
  98. db.add(_location("H2D-1 - AMS B2"))
  99. await db.commit()
  100. await _run(engine)
  101. async with sm() as db:
  102. assert await _names(db) == {"H2D-1 - AMS B2"}
  103. @pytest.mark.asyncio
  104. async def test_marks_itself_done_on_an_install_with_nothing_to_remove(engine):
  105. """Otherwise the whole catalogue is rescanned on every boot for ever."""
  106. sm = async_sessionmaker(engine, expire_on_commit=False)
  107. async with sm() as db:
  108. db.add(_location("Drybox 1"))
  109. await db.commit()
  110. await _run(engine)
  111. async with sm() as db:
  112. done = (await db.execute(text('SELECT value FROM settings WHERE "key" = :k'), {"k": FLAG})).scalar_one_or_none()
  113. assert done == "true"
  114. assert await _names(db) == {"Drybox 1"}