async def run_subject_data_store_suite(
store_factory: Callable[[], SubjectDataStore],
*,
fence_factory: Callable[[], ErasureFence],
) -> SuiteReport:
"""Run every CS-20 case against a freshly built store, and report.
A fresh store per case: sharing one would let an earlier case's leftovers
decide a later one, and a suite whose verdict depends on execution order is
not evidence.
"""
cases: list[SuiteCase] = []
participant_id = store_factory().describe().participant_id
cases.append(
SuiteCase("describe-declares-a-participant-id", bool(participant_id))
)
# Every case after this one commits through the store's conditional-write
# entry point (EFX-ER-3). A store that does not declare one is *reported*
# here rather than crashing the suite three cases later: an integrator who
# implemented the published port and got an AttributeError learns nothing
# about which obligation they missed.
if not isinstance(store_factory(), SubjectDataStoreWriter):
cases.append(
SuiteCase(
"declares-a-conditional-write-entry-point",
False,
"no `write(scope, record, *, fence, expected_generation)`; the "
"remaining cases commit through it and were not run",
)
)
return SuiteReport(
participant_id=participant_id, subject=_SUBJECT, cases=tuple(cases)
)
cases.append(SuiteCase("declares-a-conditional-write-entry-point", True))
store, fence = store_factory(), fence_factory()
outcome = await _write(store, fence, _SUBJECT, "one")
cases.append(
SuiteCase("write-commits-at-the-current-generation", bool(outcome.committed))
)
store, fence = store_factory(), fence_factory()
await _write(store, fence, _SUBJECT, "mine")
await _write(store, fence, _CHILD, "child")
await _write(store, fence, _NEIGHBOUR, "not-mine")
fragment = await store.export_subject(_SUBJECT)
bodies = {record.get("body") for record in fragment.records}
cases.append(
SuiteCase(
"export-is-subtree-scoped",
not fragment.exportable or bodies == {"mine", "child"},
f"exported {sorted(str(b) for b in bodies)}",
)
)
store, fence = store_factory(), fence_factory()
await _write(store, fence, _SUBJECT, "one")
await fence.publish_tombstone(_SUBJECT.scope_key, reason="cs-20")
receipt = await store.erase_subject(_SUBJECT, fence)
absent = await store.confirm_absent(_SUBJECT)
cases.append(
SuiteCase(
"erase-then-confirm-absent",
bool(receipt.confirmed_absent and absent),
f"erased={receipt.erased}",
)
)
store, fence = store_factory(), fence_factory()
await fence.publish_tombstone(_SUBJECT.scope_key, reason="cs-20")
denied = await _write(store, fence, _SUBJECT, "after")
cases.append(
SuiteCase(
"write-denied-after-tombstone",
not denied.committed and await store.confirm_absent(_SUBJECT),
)
)
# The sub-scope version of the same case, and the one an adapter is most
# likely to fail: erasure is subtree-scoped, so the tenant tombstone has to
# refuse a write at the child too. A store (or a fence) that matches the
# exact key lets the child write commit, and the subtree-scoped export then
# returns the subject that was just erased.
store, fence = store_factory(), fence_factory()
await fence.publish_tombstone(_SUBJECT.scope_key, reason="cs-20")
denied_child = await _write(store, fence, _CHILD, "after-child")
exported = await store.export_subject(_SUBJECT)
cases.append(
SuiteCase(
"write-denied-after-ancestor-tombstone",
not denied_child.committed
and not exported.records
and await store.confirm_absent(_SUBJECT),
f"child={_CHILD.scope_key} tombstone={_SUBJECT.scope_key}",
)
)
cases.append(await _concurrency_case(store_factory, fence_factory))
return SuiteReport(
participant_id=participant_id, subject=_SUBJECT, cases=tuple(cases)
)