176 lines
6.2 KiB
Python
176 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from sqlalchemy import Integer, String, create_engine
|
|
from sqlalchemy.orm import Mapped, Session, mapped_column, sessionmaker
|
|
from sqlalchemy.pool import StaticPool
|
|
|
|
from govoplan_core.core.concurrency import (
|
|
MissingPreconditionError,
|
|
RevisionConflictError,
|
|
assert_revision_precondition,
|
|
claim_revision,
|
|
if_match_matches,
|
|
strong_resource_etag,
|
|
three_way_merge,
|
|
)
|
|
from govoplan_core.db.base import Base
|
|
|
|
|
|
class _RevisionFixture(Base):
|
|
__tablename__ = "test_revision_fixture"
|
|
|
|
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
|
edit_revision: Mapped[int] = mapped_column(Integer, default=1, nullable=False)
|
|
value: Mapped[str] = mapped_column(String(100), nullable=False)
|
|
|
|
|
|
class OptimisticConcurrencyTests(unittest.TestCase):
|
|
def test_strong_etags_and_if_match_use_strong_comparison(self) -> None:
|
|
etag = strong_resource_etag("campaign_version", "version-1", 3)
|
|
|
|
self.assertTrue(if_match_matches(etag, etag))
|
|
self.assertTrue(if_match_matches(f'"other", {etag}', etag))
|
|
self.assertFalse(if_match_matches(f"W/{etag}", etag))
|
|
self.assertNotEqual(
|
|
etag,
|
|
strong_resource_etag("campaign_version", "version-1", 4),
|
|
)
|
|
with self.assertRaises(MissingPreconditionError):
|
|
assert_revision_precondition(
|
|
None,
|
|
resource_type="campaign_version",
|
|
resource_id="version-1",
|
|
submitted_base_revision=3,
|
|
)
|
|
|
|
def test_compare_and_set_allows_only_one_writer_for_a_revision(self) -> None:
|
|
engine = create_engine(
|
|
"sqlite+pysqlite://",
|
|
connect_args={"check_same_thread": False},
|
|
poolclass=StaticPool,
|
|
)
|
|
_RevisionFixture.__table__.create(engine)
|
|
session_factory = sessionmaker(bind=engine, class_=Session)
|
|
with session_factory() as session:
|
|
session.add(_RevisionFixture(id="resource-1", value="base"))
|
|
session.commit()
|
|
next_revision = claim_revision(
|
|
session,
|
|
model=_RevisionFixture,
|
|
filters=(_RevisionFixture.id == "resource-1",),
|
|
revision_attribute="edit_revision",
|
|
expected_revision=1,
|
|
resource_type="fixture",
|
|
resource_id="resource-1",
|
|
)
|
|
session.commit()
|
|
self.assertEqual(next_revision, 2)
|
|
|
|
with self.assertRaises(RevisionConflictError) as raised:
|
|
claim_revision(
|
|
session,
|
|
model=_RevisionFixture,
|
|
filters=(_RevisionFixture.id == "resource-1",),
|
|
revision_attribute="edit_revision",
|
|
expected_revision=1,
|
|
resource_type="fixture",
|
|
resource_id="resource-1",
|
|
)
|
|
self.assertEqual(raised.exception.current_revision, 2)
|
|
engine.dispose()
|
|
|
|
def test_disjoint_map_and_stable_id_collection_changes_merge(self) -> None:
|
|
base = {
|
|
"campaign": {"name": "Original", "description": "Base"},
|
|
"entries": {
|
|
"inline": [
|
|
{"id": "one", "name": "One", "active": True},
|
|
{"id": "two", "name": "Two", "active": True},
|
|
]
|
|
},
|
|
}
|
|
local = {
|
|
**base,
|
|
"campaign": {"name": "Local", "description": "Base"},
|
|
"entries": {
|
|
"inline": [
|
|
{"id": "one", "name": "One", "active": False},
|
|
{"id": "two", "name": "Two", "active": True},
|
|
]
|
|
},
|
|
}
|
|
current = {
|
|
**base,
|
|
"campaign": {"name": "Original", "description": "Remote"},
|
|
"entries": {
|
|
"inline": [
|
|
{"id": "one", "name": "One", "active": True},
|
|
{"id": "two", "name": "Remote Two", "active": True},
|
|
]
|
|
},
|
|
}
|
|
|
|
result = three_way_merge(base, local, current)
|
|
|
|
self.assertTrue(result.merged)
|
|
self.assertEqual(result.value["campaign"], {"name": "Local", "description": "Remote"})
|
|
self.assertEqual(result.value["entries"]["inline"][0]["active"], False)
|
|
self.assertEqual(result.value["entries"]["inline"][1]["name"], "Remote Two")
|
|
|
|
def test_same_path_unkeyed_collection_and_protected_edits_conflict(self) -> None:
|
|
same_path = three_way_merge(
|
|
{"name": "Base"},
|
|
{"name": "Local"},
|
|
{"name": "Remote"},
|
|
)
|
|
self.assertEqual(same_path.conflicts[0].kind, "same_path_changed")
|
|
|
|
unkeyed = three_way_merge(
|
|
{"values": ["a"]},
|
|
{"values": ["a", "local"]},
|
|
{"values": ["a", "remote"]},
|
|
)
|
|
self.assertEqual(unkeyed.conflicts[0].kind, "unkeyed_collection")
|
|
|
|
protected = three_way_merge(
|
|
{"workflow_state": "draft", "name": "Base"},
|
|
{"workflow_state": "ready", "name": "Base"},
|
|
{"workflow_state": "draft", "name": "Remote"},
|
|
protected_paths=("/workflow_state",),
|
|
)
|
|
self.assertEqual(protected.conflicts[0].kind, "protected_path")
|
|
|
|
def test_delete_versus_edit_and_conflicting_reorder_remain_explicit(self) -> None:
|
|
base = [{"id": "one", "value": "1"}, {"id": "two", "value": "2"}]
|
|
delete_vs_edit = three_way_merge(
|
|
base,
|
|
[{"id": "two", "value": "2"}],
|
|
[{"id": "one", "value": "remote"}, {"id": "two", "value": "2"}],
|
|
)
|
|
self.assertEqual(delete_vs_edit.conflicts[0].kind, "delete_vs_edit")
|
|
|
|
reordered = three_way_merge(
|
|
[
|
|
{"id": "one"},
|
|
{"id": "two"},
|
|
{"id": "three"},
|
|
],
|
|
[
|
|
{"id": "two"},
|
|
{"id": "one"},
|
|
{"id": "three"},
|
|
],
|
|
[
|
|
{"id": "one"},
|
|
{"id": "three"},
|
|
{"id": "two"},
|
|
],
|
|
)
|
|
self.assertEqual(reordered.conflicts[0].kind, "collection_reorder")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|