Systematic rename of all semantic anchors (#region, [DEF], @RELATION) across 1400+ files — backend Python, frontend Svelte/TS, specs, docs: - Flat anchors become Namespace.Module.Entity - @RELATION references updated to match new anchor paths - Zero business logic changes
227 lines
9.1 KiB
Python
227 lines
9.1 KiB
Python
# #region Test.CheckMigrationChain.TestCheckMigrationChain [C:2] [TYPE Module] [SEMANTICS test,alembic,migration,chain]
|
|
# @BRIEF Tests for migration chain validation logic (mock alembic, no real DB).
|
|
# @RELATION BINDS_TO -> [Tooling.CheckMigrationChain]
|
|
# @TEST_EDGE: check_migration_heads_single -> OK prints single head
|
|
# @TEST_EDGE: check_migration_heads_multiple -> Exits 1 with error
|
|
# @TEST_EDGE: check_migration_heads_empty -> Exits 1 with error
|
|
# @TEST_EDGE: chain_integrity_ok -> Traverses clean chain
|
|
# @TEST_EDGE: chain_integrity_broken_link -> Exits 1
|
|
# @TEST_EDGE: chain_integrity_merge_migration -> Handles tuple down_revision
|
|
# @TEST_EDGE: chain_integrity_broken_merge -> Exits 1
|
|
# @TEST_EDGE: main_runs_all_checks -> Calls both functions
|
|
# @TEST_EDGE: main_with_multiple_heads -> Exits early
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch, call
|
|
|
|
import pytest
|
|
|
|
# Ensure src is importable
|
|
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
|
|
|
|
|
|
# =============================================================================
|
|
# check_migration_heads
|
|
# =============================================================================
|
|
|
|
class TestCheckMigrationHeads:
|
|
"""Tests for check_migration_chain.check_migration_heads()."""
|
|
|
|
# Alembic imports are inside function body, so patch the real full paths
|
|
@patch("alembic.script.ScriptDirectory")
|
|
@patch("alembic.config.Config")
|
|
def test_single_head(self, MockAlembicConfig, MockScriptDir, capsys):
|
|
"""Happy: single head prints OK."""
|
|
mock_script = MagicMock()
|
|
mock_script.get_heads.return_value = ["abc123"]
|
|
rev = MagicMock()
|
|
rev.doc = "Initial migration"
|
|
mock_script.get_revision.return_value = rev
|
|
MockScriptDir.from_config.return_value = mock_script
|
|
|
|
from src.scripts.check_migration_chain import check_migration_heads
|
|
check_migration_heads()
|
|
|
|
captured = capsys.readouterr()
|
|
assert "OK: Single migration head" in captured.out
|
|
assert "abc123" in captured.out
|
|
|
|
@patch("alembic.script.ScriptDirectory")
|
|
@patch("alembic.config.Config")
|
|
def test_multiple_heads_exits(self, MockAlembicConfig, MockScriptDir):
|
|
"""Edge: multiple heads prints error and exits 1."""
|
|
mock_script = MagicMock()
|
|
mock_script.get_heads.return_value = ["head1", "head2"]
|
|
rev1 = MagicMock()
|
|
rev1.doc = "Migration A"
|
|
rev2 = MagicMock()
|
|
rev2.doc = "Migration B"
|
|
|
|
def get_revision_side_effect(rev_id):
|
|
return {"head1": rev1, "head2": rev2}[rev_id]
|
|
|
|
mock_script.get_revision.side_effect = get_revision_side_effect
|
|
MockScriptDir.from_config.return_value = mock_script
|
|
|
|
from src.scripts.check_migration_chain import check_migration_heads
|
|
with pytest.raises(SystemExit) as exc:
|
|
check_migration_heads()
|
|
assert exc.value.code == 1
|
|
|
|
@patch("alembic.script.ScriptDirectory")
|
|
@patch("alembic.config.Config")
|
|
def test_no_heads_exits(self, MockAlembicConfig, MockScriptDir):
|
|
"""Edge: no heads prints error and exits 1."""
|
|
mock_script = MagicMock()
|
|
mock_script.get_heads.return_value = []
|
|
MockScriptDir.from_config.return_value = mock_script
|
|
|
|
from src.scripts.check_migration_chain import check_migration_heads
|
|
with pytest.raises(SystemExit) as exc:
|
|
check_migration_heads()
|
|
assert exc.value.code == 1
|
|
|
|
|
|
# =============================================================================
|
|
# check_migration_chain_integrity
|
|
# =============================================================================
|
|
|
|
class TestCheckMigrationChainIntegrity:
|
|
"""Tests for check_migration_chain.check_migration_chain_integrity()."""
|
|
|
|
def _make_revision(self, rev_id, down_revision, doc=""):
|
|
rev = MagicMock()
|
|
rev.revision = rev_id
|
|
rev.down_revision = down_revision
|
|
rev.doc = doc
|
|
return rev
|
|
|
|
@patch("alembic.script.ScriptDirectory")
|
|
@patch("alembic.config.Config")
|
|
def test_chain_ok(self, MockAlembicConfig, MockScriptDir, capsys):
|
|
"""Happy: linear chain traversed without error."""
|
|
head = self._make_revision("c", "b", "Third")
|
|
middle = self._make_revision("b", "a", "Second")
|
|
root = self._make_revision("a", None, "First")
|
|
|
|
mock_script = MagicMock()
|
|
mock_script.get_heads.return_value = ["c"]
|
|
|
|
def get_revision(rev_id):
|
|
mapping = {"c": head, "b": middle, "a": root}
|
|
return mapping[rev_id]
|
|
|
|
mock_script.get_revision.side_effect = get_revision
|
|
MockScriptDir.from_config.return_value = mock_script
|
|
|
|
from src.scripts.check_migration_chain import check_migration_chain_integrity
|
|
check_migration_chain_integrity()
|
|
|
|
captured = capsys.readouterr()
|
|
assert "Chain integrity verified" in captured.out
|
|
assert "3 migrations traversed" in captured.out
|
|
|
|
@patch("alembic.script.ScriptDirectory")
|
|
@patch("alembic.config.Config")
|
|
def test_broken_link_exits(self, MockAlembicConfig, MockScriptDir):
|
|
"""Edge: missing down_revision exits 1."""
|
|
head = self._make_revision("c", "bogus", "Broken")
|
|
|
|
mock_script = MagicMock()
|
|
mock_script.get_heads.return_value = ["c"]
|
|
|
|
def get_revision(rev_id):
|
|
if rev_id == "c":
|
|
return head
|
|
raise Exception(f"Revision {rev_id} not found")
|
|
|
|
mock_script.get_revision.side_effect = get_revision
|
|
MockScriptDir.from_config.return_value = mock_script
|
|
|
|
from src.scripts.check_migration_chain import check_migration_chain_integrity
|
|
with pytest.raises(SystemExit) as exc:
|
|
check_migration_chain_integrity()
|
|
assert exc.value.code == 1
|
|
|
|
@patch("alembic.script.ScriptDirectory")
|
|
@patch("alembic.config.Config")
|
|
def test_merge_migration_handled(self, MockAlembicConfig, MockScriptDir, capsys):
|
|
"""Edge: merge migration (tuple down_revision) traversed correctly."""
|
|
merge = self._make_revision("merge", ("a", "b"), "Merge")
|
|
parent_a = self._make_revision("a", None, "Root A")
|
|
parent_b = self._make_revision("b", None, "Root B")
|
|
|
|
mock_script = MagicMock()
|
|
mock_script.get_heads.return_value = ["merge"]
|
|
|
|
def get_revision(rev_id):
|
|
mapping = {"merge": merge, "a": parent_a, "b": parent_b}
|
|
return mapping[rev_id]
|
|
|
|
mock_script.get_revision.side_effect = get_revision
|
|
MockScriptDir.from_config.return_value = mock_script
|
|
|
|
from src.scripts.check_migration_chain import check_migration_chain_integrity
|
|
check_migration_chain_integrity()
|
|
|
|
captured = capsys.readouterr()
|
|
assert "Chain integrity verified" in captured.out
|
|
assert "3 migrations traversed" in captured.out
|
|
|
|
@patch("alembic.script.ScriptDirectory")
|
|
@patch("alembic.config.Config")
|
|
def test_broken_merge_link_exits(self, MockAlembicConfig, MockScriptDir):
|
|
"""Edge: merge migration with missing parent exits 1."""
|
|
merge = self._make_revision("merge", ("a", "ghost"), "Merge")
|
|
|
|
mock_script = MagicMock()
|
|
mock_script.get_heads.return_value = ["merge"]
|
|
|
|
def get_revision(rev_id):
|
|
if rev_id == "merge":
|
|
return merge
|
|
if rev_id == "a":
|
|
return self._make_revision("a", None, "Root A")
|
|
raise Exception(f"Revision {rev_id} not found")
|
|
|
|
mock_script.get_revision.side_effect = get_revision
|
|
MockScriptDir.from_config.return_value = mock_script
|
|
|
|
from src.scripts.check_migration_chain import check_migration_chain_integrity
|
|
with pytest.raises(SystemExit) as exc:
|
|
check_migration_chain_integrity()
|
|
assert exc.value.code == 1
|
|
|
|
|
|
# =============================================================================
|
|
# main
|
|
# =============================================================================
|
|
|
|
class TestMain:
|
|
"""Tests for check_migration_chain.main()."""
|
|
|
|
@patch("src.scripts.check_migration_chain.check_migration_chain_integrity")
|
|
@patch("src.scripts.check_migration_chain.check_migration_heads")
|
|
def test_main_calls_both(self, mock_heads, mock_integrity, capsys):
|
|
"""Happy: main() calls both checks and prints pass."""
|
|
from src.scripts.check_migration_chain import main
|
|
main()
|
|
|
|
mock_heads.assert_called_once()
|
|
mock_integrity.assert_called_once()
|
|
captured = capsys.readouterr()
|
|
assert "All checks passed" in captured.out
|
|
|
|
@patch("src.scripts.check_migration_chain.check_migration_chain_integrity")
|
|
@patch("src.scripts.check_migration_chain.check_migration_heads")
|
|
def test_main_stops_on_heads_failure(self, mock_heads, mock_integrity):
|
|
"""Edge: main exits early if check_migration_heads fails."""
|
|
mock_heads.side_effect = SystemExit(1)
|
|
|
|
from src.scripts.check_migration_chain import main
|
|
with pytest.raises(SystemExit) as exc:
|
|
main()
|
|
assert exc.value.code == 1
|
|
mock_integrity.assert_not_called()
|
|
# #endregion Test.CheckMigrationChain.TestCheckMigrationChain
|