CultLib/packages/cultcache-py/tests/test_cultcache.py
2026-10-02 21:05:38 +02:00

1591 lines
85 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import os
import tempfile
import threading
import unittest
from dataclasses import asdict, dataclass, replace
from pathlib import Path
from cultcache_py.backing_store import CultCacheEnvelope
from cultcache_py.cache import CultCacheError
from cultcache_py import (
CultCache,
JsonLinesBackingStore,
SingleFileMessagePackBackingStore,
StoreUnreadableError,
define_database_entry_type,
define_document_type,
)
from cultcache_py.interop import read_note, write_note
@dataclass
class Item:
name: str
category: str
value: int
def item_doc():
return define_document_type(
"item",
encode=lambda item: asdict(item),
decode=lambda raw: Item(**raw),
name="name",
indexes={"category": "category"},
)
class CultCacheTests(unittest.TestCase):
def test_round_trips_registered_documents(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
document = item_doc()
cache = (
CultCache.builder()
.register_document_type(document)
.add_generic_store(JsonLinesBackingStore(Path(tmp) / "cache.jsonl"))
.build()
)
cache.pull_all_backing_stores()
cache.put(document, "item:potion", Item(name="Potion", category="Consumable", value=50))
loaded = (
CultCache.builder()
.register_document_type(document)
.add_generic_store(JsonLinesBackingStore(Path(tmp) / "cache.jsonl"))
.build()
)
loaded.pull_all_backing_stores()
self.assertEqual(loaded.get_required(document, "item:potion").value, 50)
self.assertEqual(loaded.get_key_by_name(document, "Potion"), "item:potion")
self.assertEqual(loaded.get_by_index(document, "category", "Consumable").name, "Potion")
def test_global_document_uses_singleton_key(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
settings = define_document_type("settings", global_document=True)
cache = (
CultCache.builder()
.register_document_type(settings)
.add_generic_store(JsonLinesBackingStore(Path(tmp) / "cache.jsonl"))
.build()
)
cache.pull_all_backing_stores()
cache.put_global(settings, {"theme": "ash"})
self.assertEqual(cache.get_required_global(settings)["theme"], "ash")
def test_update_and_delete(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
document = item_doc()
cache = (
CultCache.builder()
.register_document_type(document)
.add_generic_store(JsonLinesBackingStore(Path(tmp) / "cache.jsonl"))
.build()
)
cache.pull_all_backing_stores()
cache.put(document, "item:potion", Item(name="Potion", category="Consumable", value=50))
cache.update(document, "item:potion", lambda item: Item(item.name, item.category, item.value + 5))
self.assertEqual(cache.get_required(document, "item:potion").value, 55)
cache.delete(document, "item:potion")
self.assertIsNone(cache.get(document, "item:potion"))
def test_incremental_lookup_updates_replace_stale_name_and_index_entries(self) -> None:
document = item_doc()
cache = CultCache()
cache.register_document_type(document)
cache.put(document, "item:potion", Item(name="Potion", category="Consumable", value=50))
cache.put(document, "item:potion", Item(name="Elixir", category="Rare", value=80))
self.assertIsNone(cache.get_key_by_name(document, "Potion"))
self.assertIsNone(cache.get_key_by_index(document, "category", "Consumable"))
self.assertEqual(cache.get_key_by_name(document, "Elixir"), "item:potion")
self.assertEqual(cache.get_by_index(document, "category", "Rare").name, "Elixir")
def test_incremental_lookup_delete_restores_duplicate_owner(self) -> None:
document = item_doc()
cache = CultCache()
cache.register_document_type(document)
cache.put(document, "item:first", Item(name="Potion", category="Consumable", value=50))
cache.put(document, "item:second", Item(name="Potion", category="Consumable", value=60))
self.assertEqual(cache.get_key_by_name(document, "Potion"), "item:second")
self.assertEqual(cache.get_key_by_index(document, "category", "Consumable"), "item:second")
cache.delete(document, "item:second")
self.assertEqual(cache.get_key_by_name(document, "Potion"), "item:first")
self.assertEqual(cache.get_key_by_index(document, "category", "Consumable"), "item:first")
def test_database_entry_formatter_uses_slot_indexed_messagepack_array(self) -> None:
try:
import msgpack # type: ignore
except ModuleNotFoundError:
self.skipTest("msgpack optional dependency is not installed")
@dataclass
class Settings:
theme: str
retries: int = 0
document = define_database_entry_type(
"settings",
[
("theme", 0),
("retries", 2, 0),
],
cls=Settings,
)
payload = document.encode_payload(Settings(theme="ash", retries=3))
self.assertEqual(msgpack.unpackb(payload, raw=False), ["ash", None, 3])
self.assertEqual(document.decode_payload(msgpack.packb(["ash"], use_bin_type=True)).retries, 0)
def test_document_catalog_entry_is_cached_after_first_derivation(self) -> None:
document = define_database_entry_type(
"settings",
[
("theme", 0),
("retries", 2, 0),
],
)
first = document.catalog_entry()
second = document.catalog_entry()
self.assertIs(first, second)
self.assertEqual([member.slot for member in first.members], [0, 2])
def test_messagepack_store_writes_v1_snapshot(self) -> None:
try:
import msgpack # type: ignore
except ModuleNotFoundError:
self.skipTest("msgpack optional dependency is not installed")
with tempfile.TemporaryDirectory() as tmp:
document = define_database_entry_type(
"settings",
[("theme", 0)],
schema_id="settings",
schema_name="settings",
schema_version="settings.v1",
)
store_path = Path(tmp) / "cache.msgpack"
cache = (
CultCache.builder()
.register_document_type(document)
.add_generic_store(SingleFileMessagePackBackingStore(store_path))
.build()
)
cache.pull_all_backing_stores()
cache.put(document, "app", {"theme": "ash"})
raw = msgpack.unpackb(store_path.read_bytes(), raw=False)
self.assertEqual(raw[0], "cultcache.store.v1")
self.assertEqual(raw[1][0][0], "settings")
self.assertEqual(raw[2][0][0], "app")
self.assertEqual(raw[2][0][1], "settings")
def test_messagepack_store_refuses_a_record_whose_schema_the_catalog_does_not_publish(self) -> None:
try:
import msgpack # type: ignore
except ModuleNotFoundError:
self.skipTest("msgpack optional dependency is not installed")
with tempfile.TemporaryDirectory() as tmp:
document = define_database_entry_type(
"runtime-policy",
[
("schema_version", 0),
("name", 1),
("value", 2),
],
schema_id="schema-current",
schema_name="tests.schema_stamped_entry",
schema_version="tests.schema_stamped_entry.v1",
)
store_path = Path(tmp) / "missing-catalog.msgpack"
store_path.write_bytes(msgpack.packb([
"cultcache.store.v1",
[],
[[
"record-1",
"sha256:stale-schema-id-from-cold-record",
"2026-06-25T12:00:00Z",
msgpack.packb([
"tests.schema_stamped_entry.v1",
"schema-stamped",
"still readable",
], use_bin_type=True),
]],
], use_bin_type=True))
cache = (
CultCache.builder()
.register_document_type(document)
.add_generic_store(SingleFileMessagePackBackingStore(store_path))
.build()
)
# The payload opens with a schema version, and the catalog is the store's own description: no recovery.
with self.assertRaises(StoreUnreadableError):
cache.pull_all_backing_stores()
def _foreign_id_document(self):
return define_database_entry_type(
"tests.foreign-id",
[("name", 0)],
schema_id="tests.foreign-id.current",
schema_name="tests.foreign-id",
schema_version="tests.foreign_id.v1",
compatible_schema_ids=["tests.foreign-id.current", "tests.foreign-id.older"],
)
def _open_foreign_id_store(self, document, path: Path) -> CultCache:
cache = CultCache.builder().register_document_type(document).add_generic_store(
SingleFileMessagePackBackingStore(path)
).build()
cache.pull_all_backing_stores()
return cache
# A record put under an id its schema only lists as compatible is stamped with the registered id, and the store the write
# leaves opens and takes the next write.
def test_put_envelope_under_a_compatible_schema_id_writes_a_store_that_reopens(self) -> None:
import msgpack # type: ignore
from cultcache_py import CultCacheSchemaCatalogEntry
document = self._foreign_id_document()
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.msgpack"
cache = self._open_foreign_id_store(document, path)
payload = document.encode_payload({"name": "foreign"})
foreign_entry = CultCacheSchemaCatalogEntry(
schema_id="tests.foreign-id.older",
schema_name="tests.foreign-id",
schema_version="tests.foreign_id.v1",
content_hash="tests.foreign-id.older",
canonical_schema_json="",
compatible_schema_ids=("tests.foreign-id.older",),
members=(),
)
cache.put_envelope(
document,
CultCacheEnvelope(
key="foreign",
type="tests.foreign-id",
payload=payload,
stored_at="2026-09-30T00:00:00Z",
schema_id="tests.foreign-id.older",
catalog_entry=foreign_entry,
),
)
stored = msgpack.unpackb(path.read_bytes(), raw=False)
self.assertEqual([entry[0] for entry in stored[1]], ["tests.foreign-id.current"])
self.assertEqual(stored[2][0][1], "tests.foreign-id.current")
reopened = self._open_foreign_id_store(document, path)
self.assertEqual(reopened.get_required(document, "foreign")["name"], "foreign")
reopened.put(document, "next", {"name": "next"})
self.assertEqual(self._open_foreign_id_store(document, path).get_required(document, "next")["name"], "next")
def test_the_store_publishes_a_records_schema_id_even_when_the_envelopes_catalog_entry_does_not(self) -> None:
import msgpack # type: ignore
from cultcache_py import CultCacheSchemaCatalogEntry
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.msgpack"
store = SingleFileMessagePackBackingStore(path)
store.push(CultCacheEnvelope(
key="k",
type="tests.store-publishes",
payload=msgpack.packb({"name": "k"}, use_bin_type=True),
stored_at="2026-09-30T00:00:00Z",
schema_id="tests.store-publishes.record",
catalog_entry=CultCacheSchemaCatalogEntry(
schema_id="tests.store-publishes.other",
schema_name="tests.store-publishes",
schema_version="tests.store_publishes.v1",
content_hash="tests.store-publishes.other",
canonical_schema_json="",
compatible_schema_ids=("tests.store-publishes.other",),
members=(),
),
))
stored = msgpack.unpackb(path.read_bytes(), raw=False)
self.assertEqual(stored[2][0][1], "tests.store-publishes.record")
self.assertEqual([entry[0] for entry in stored[1]], ["tests.store-publishes.record"])
self.assertEqual(len(SingleFileMessagePackBackingStore(path).pull_all()), 1)
# The catalog a write leaves is derived from its records: one entry per carried id, chosen and never merged, the same whatever
# order the records arrive in. A registered (supplied) entry wins over an arrived (read back) entry with the same own id.
@staticmethod
def _writer_entry(schema_id: str, name: str, content_hash: str, compatible: tuple[str, ...]):
from cultcache_py import CultCacheSchemaCatalogEntry
return CultCacheSchemaCatalogEntry(
schema_id=schema_id, schema_name=name, schema_version=name + ".v1", content_hash=content_hash,
canonical_schema_json="", compatible_schema_ids=compatible, members=(),
)
def _writer_record(self, key: str, type: str, schema_id: str, catalog_entry=None) -> CultCacheEnvelope:
import msgpack # type: ignore
return CultCacheEnvelope(
key=key, type=type, payload=msgpack.packb({"key": key}, use_bin_type=True),
stored_at="2026-09-30T00:00:00Z", schema_id=schema_id, catalog_entry=catalog_entry,
)
@staticmethod
def _raw_entry(entry) -> list:
return [entry.schema_id, entry.schema_name, entry.schema_version, entry.content_hash, "", list(entry.compatible_schema_ids), []]
def test_a_registered_entry_is_written_as_it_is_over_the_arrived_entries_with_the_same_own_id_in_either_order(self) -> None:
import msgpack # type: ignore
stale1 = self._writer_entry("tests.x", "tests.n", "a-stale-1", ("tests.x", "old"))
stale2 = self._writer_entry("tests.x", "tests.n", "a-stale-2", ("tests.x",))
registered = self._writer_record("a", "tests.n", "tests.x", self._writer_entry("tests.x", "tests.n", "fresh", ("tests.x",)))
for entries in ([stale1, stale2], [stale2, stale1]):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.msgpack"
path.write_bytes(msgpack.packb([
"cultcache.store.v1", [self._raw_entry(entry) for entry in entries],
[["b", "tests.x", "2026-09-30T00:00:00Z", msgpack.packb({"b": 1}, use_bin_type=True)]],
], use_bin_type=True))
SingleFileMessagePackBackingStore(path).push(registered)
catalog = msgpack.unpackb(path.read_bytes(), raw=False)[1]
self.assertEqual(len(catalog), 1)
self.assertEqual(catalog[0][3], "fresh")
self.assertEqual(catalog[0][5], ["tests.x"], "written as chosen, no union with the arrived entries' ids")
def test_a_rename_with_a_stable_schema_id_a_whole_view_write_names_the_registered_schema(self) -> None:
import msgpack # type: ignore
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.msgpack"
path.write_bytes(msgpack.packb([
"cultcache.store.v1", [self._raw_entry(self._writer_entry("tests.x", "tests.old", "stale", ("tests.x",)))],
[["a", "tests.x", "2026-09-30T00:00:00Z", msgpack.packb({"a": 1}, use_bin_type=True)]],
], use_bin_type=True))
SingleFileMessagePackBackingStore(path).push_all(
[self._writer_record("a", "tests.old", "tests.x", self._writer_entry("tests.x", "tests.new", "fresh", ("tests.x",)))]
)
catalog = msgpack.unpackb(path.read_bytes(), raw=False)[1]
self.assertEqual([(entry[1], entry[3]) for entry in catalog], [("tests.new", "fresh")])
def test_two_entries_of_one_tier_that_share_an_own_id_and_disagree_on_the_schema_name_refuse_the_write_typed(self) -> None:
from cultcache_py import SchemaConflictError
first = self._writer_record("b", "tests.first", "tests.x", self._writer_entry("tests.x", "tests.first", "h1", ("tests.x",)))
second = self._writer_record("a", "tests.first", "tests.x", self._writer_entry("tests.x", "tests.second", "h2", ("tests.x",)))
for order in ([first, second], [second, first]):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.msgpack"
with self.assertRaises(SchemaConflictError) as refused:
SingleFileMessagePackBackingStore(path).push_all(order)
self.assertEqual(refused.exception.schema_id, "tests.x")
self.assertEqual(refused.exception.record_key, "a")
self.assertEqual(refused.exception.schema_names, ["tests.first", "tests.second"])
self.assertFalse(path.exists())
def test_records_of_different_types_under_one_schema_id_refuse_the_write_typed_and_no_record_changes_type(self) -> None:
from cultcache_py import SchemaConflictError
a = self._writer_record("a", "tests.n", "tests.y", self._writer_entry("tests.x", "tests.n", "h1", ("tests.x", "tests.y")))
b = self._writer_record("b", "tests.m", "tests.y", self._writer_entry("tests.y", "tests.m", "h2", ("tests.y",)))
for order in ([a, b], [b, a]):
with tempfile.TemporaryDirectory() as tmp:
with self.assertRaises(SchemaConflictError) as refused:
SingleFileMessagePackBackingStore(Path(tmp) / "store.msgpack").push_all(order)
self.assertEqual(refused.exception.schema_id, "tests.y")
self.assertEqual(refused.exception.schema_names, ["tests.m", "tests.n"])
def test_an_entry_that_owns_an_id_is_chosen_over_one_that_lists_it(self) -> None:
import msgpack # type: ignore
owner = self._writer_record("a", "tests.n", "tests.y", self._writer_entry("tests.y", "tests.n", "h1", ("tests.y",)))
lister = self._writer_record("b", "tests.n", "tests.x", self._writer_entry("tests.x", "tests.n", "h2", ("tests.x", "tests.y")))
for order in ([lister, owner], [owner, lister]):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.msgpack"
SingleFileMessagePackBackingStore(path).push_all(order)
catalog = msgpack.unpackb(path.read_bytes(), raw=False)[1]
self.assertEqual(sorted((entry[0], entry[5]) for entry in catalog), [("tests.x", ["tests.x", "tests.y"]), ("tests.y", ["tests.y"])])
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.msgpack"
SingleFileMessagePackBackingStore(path).push_all([owner])
self.assertEqual([entry[0] for entry in msgpack.unpackb(path.read_bytes(), raw=False)[1]], ["tests.y"])
def test_entries_of_one_tier_that_tie_are_taken_in_one_fixed_order_and_a_registered_lister_beats_an_arrived_one(self) -> None:
import msgpack # type: ignore
# The content hash orders them against the order their compatible ids would give: h1 sorts first, its list sorts last.
one = self._writer_record("a", "tests.n", "tests.x", self._writer_entry("tests.x", "tests.n", "h2", ("tests.x",)))
two = self._writer_record("b", "tests.n", "tests.x", self._writer_entry("tests.x", "tests.n", "h1", ("tests.x", "tests.y")))
for order in ([one, two], [two, one]):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.msgpack"
SingleFileMessagePackBackingStore(path).push_all(order)
self.assertEqual([entry[3] for entry in msgpack.unpackb(path.read_bytes(), raw=False)[1]], ["h1"])
# z sits under old, which the arrived entry (id x) lists; the supplied entry (id r) lists it too: the registered one publishes it.
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.msgpack"
path.write_bytes(msgpack.packb([
"cultcache.store.v1", [self._raw_entry(self._writer_entry("tests.x", "tests.n", "stale", ("tests.x", "old")))],
[["z", "old", "2026-09-30T00:00:00Z", msgpack.packb({"z": 1}, use_bin_type=True)]],
], use_bin_type=True))
SingleFileMessagePackBackingStore(path).push(
self._writer_record("a", "tests.n", "tests.r", self._writer_entry("tests.r", "tests.n", "fresh", ("tests.r", "old")))
)
self.assertEqual([entry[0] for entry in msgpack.unpackb(path.read_bytes(), raw=False)[1]], ["tests.r"])
# A registered entry that owns an id keeps it against an arrived entry that owns it too and lists the id another record sits
# under, whichever of the two ids sorts first: the arrived entry's list is not merged in, so the record under the listed id is
# published by no chosen entry.
def test_an_arrived_entry_that_owns_a_registered_id_does_not_replace_it_nor_publish_what_it_lists(self) -> None:
import msgpack # type: ignore
from cultcache_py import SchemaConflictError
for own_id, listed_id in (("tests.a", "tests.b"), ("tests.b", "tests.a")):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.msgpack"
path.write_bytes(msgpack.packb([
"cultcache.store.v1", [self._raw_entry(self._writer_entry(own_id, "tests.arrived", "stale", (own_id, listed_id)))],
[["k2", listed_id, "2026-09-30T00:00:00Z", msgpack.packb({"k": 2}, use_bin_type=True)]],
], use_bin_type=True))
before = path.read_bytes()
with self.assertRaises(SchemaConflictError) as refused:
SingleFileMessagePackBackingStore(path).push(
self._writer_record("k1", "tests.registered", own_id, self._writer_entry(own_id, "tests.registered", "fresh", (own_id,)))
)
self.assertEqual(refused.exception.schema_id, listed_id)
self.assertEqual(refused.exception.record_key, "k2")
self.assertEqual(path.read_bytes(), before, "the file is left as it was")
# Ids are taken in sorted order, so which entry survives, and so which id the refusal names, does not depend on the order the
# records arrive in.
def test_the_refusal_names_the_same_id_whatever_order_the_records_arrive_in(self) -> None:
from cultcache_py import SchemaConflictError
one = self._writer_record("r1", "tests.n", "tests.y", self._writer_entry("tests.x", "tests.n", "h1", ("tests.x", "tests.y")))
two = self._writer_record("r2", "tests.n", "tests.z", self._writer_entry("tests.x", "tests.n", "h2", ("tests.x", "tests.z")))
for order in ([one, two], [two, one]):
with tempfile.TemporaryDirectory() as tmp:
with self.assertRaises(SchemaConflictError) as refused:
SingleFileMessagePackBackingStore(Path(tmp) / "store.msgpack").push_all(order)
self.assertEqual(refused.exception.schema_id, "tests.y")
self.assertEqual(refused.exception.record_key, "r1")
# A write may not retype records already in the file: across tiers the id keeps the type its records resolve to, unless the
# entries of both tiers own it, which is a rename under a stable id and the registered descriptor wins.
def test_a_write_that_would_change_the_type_an_existing_record_resolves_to_refuses_typed_and_leaves_the_file(self) -> None:
import msgpack # type: ignore
from cultcache_py import SchemaConflictError
cases = [
("registered entry lists the id, arrived entry owns it",
self._writer_entry("tests.x", "tests.arrived", "h1", ("tests.x",)),
self._writer_record("a", "tests.registered", "tests.x", self._writer_entry("tests.r", "tests.registered", "h2", ("tests.r", "tests.x")))),
("registered entry owns the id, arrived entry lists it",
self._writer_entry("tests.r", "tests.arrived", "h1", ("tests.r", "tests.x")),
self._writer_record("a", "tests.registered", "tests.x", self._writer_entry("tests.x", "tests.registered", "h2", ("tests.x",)))),
]
payload = msgpack.packb({"z": 1}, use_bin_type=True)
with tempfile.TemporaryDirectory() as tmp:
for name, catalog, registered in cases:
path = Path(tmp) / (name.split(",")[0].replace(" ", "-") + ".msgpack")
path.write_bytes(msgpack.packb(
["cultcache.store.v1", [self._raw_entry(catalog)], [["z", "tests.x", "2026-09-30T00:00:00Z", payload]]], use_bin_type=True))
before = path.read_bytes()
with self.assertRaises(SchemaConflictError, msg=name) as refused:
SingleFileMessagePackBackingStore(path).push(registered)
self.assertEqual(refused.exception.schema_id, "tests.x", name)
self.assertEqual(sorted(refused.exception.schema_names), ["tests.arrived", "tests.registered"], name)
self.assertEqual(path.read_bytes(), before, name)
# The same registered record renames the schema when both tiers own the id: it writes, and the reader finds every record.
path = Path(tmp) / "rename.msgpack"
path.write_bytes(msgpack.packb([
"cultcache.store.v1", [self._raw_entry(self._writer_entry("tests.x", "tests.arrived", "h1", ("tests.x",)))],
[["z", "tests.x", "2026-09-30T00:00:00Z", payload]],
], use_bin_type=True))
SingleFileMessagePackBackingStore(path).push(
self._writer_record("a", "tests.registered", "tests.x", self._writer_entry("tests.x", "tests.registered", "h2", ("tests.x",)))
)
self.assertEqual([(entry[0], entry[1]) for entry in msgpack.unpackb(path.read_bytes(), raw=False)[1]], [("tests.x", "tests.registered")])
self.assertEqual(len(SingleFileMessagePackBackingStore(path).pull_all()), 2)
def test_a_record_no_chosen_entry_publishes_refuses_the_write_typed_and_leaves_the_file(self) -> None:
import msgpack # type: ignore
from cultcache_py import SchemaConflictError
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.msgpack"
path.write_bytes(msgpack.packb([
"cultcache.store.v1", [self._raw_entry(self._writer_entry("tests.x", "tests.legacy", "stale", ("tests.x", "old")))],
[["z", "old", "2026-09-30T00:00:00Z", msgpack.packb({"z": 1}, use_bin_type=True)]],
], use_bin_type=True))
before = path.read_bytes()
with self.assertRaises(SchemaConflictError) as refused:
SingleFileMessagePackBackingStore(path).push(
self._writer_record("a", "tests.legacy", "tests.x", self._writer_entry("tests.x", "tests.legacy", "fresh", ("tests.x",)))
)
self.assertEqual(refused.exception.schema_id, "old")
self.assertEqual(refused.exception.record_key, "z")
self.assertEqual(path.read_bytes(), before)
def test_an_id_an_entry_owns_names_that_entry_not_one_that_lists_it_as_compatible(self) -> None:
for vector in ("own-id-over-compatible-v3.bin", "compatible-before-owner-v3.bin"):
store = SingleFileMessagePackBackingStore(self._C2A_VECTORS / "readability" / vector)
self.assertEqual([envelope.type for envelope in store.pull_all()], ["vectors.item", "vectors.item"], vector)
def test_a_record_loaded_under_a_compatible_schema_id_is_written_back_under_the_id_its_catalog_entry_carries(self) -> None:
import msgpack # type: ignore
document = self._foreign_id_document()
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.msgpack"
path.write_bytes(msgpack.packb([
"cultcache.store.v1",
[["tests.foreign-id.current", "tests.foreign-id", "tests.foreign_id.v1", "tests.foreign-id.current", "",
["tests.foreign-id.current", "tests.foreign-id.older"], []]],
[["old", "tests.foreign-id.older", "2026-09-30T00:00:00Z", document.encode_payload({"name": "old"})]],
], use_bin_type=True))
cache = self._open_foreign_id_store(document, path)
self.assertEqual(cache.get_required_envelope(document, "old").schema_id, "tests.foreign-id.current")
cache.put(document, "next", {"name": "next"})
reopened = self._open_foreign_id_store(document, path)
self.assertEqual(reopened.get_required(document, "old")["name"], "old")
self.assertEqual(reopened.get_required(document, "next")["name"], "next")
# The cache restamps a record on load, so a store's writer never has to: a record loaded under an id the registered document
# only lists is held, and written back by a whole write, under the id its catalog entry carries. A document that does not list
# the id a record sits under cannot have that record published by a write that leaves it in place: the put is refused, typed.
def test_a_cache_that_does_not_list_the_id_a_loaded_record_sits_under_is_refused_a_put_and_the_file_is_left(self) -> None:
import msgpack # type: ignore
from cultcache_py import SchemaConflictError
document = define_database_entry_type(
"tests.foreign-id", [("name", 0)], schema_id="tests.foreign-id.current",
schema_name="tests.foreign-id", schema_version="tests.foreign_id.v1",
)
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.msgpack"
path.write_bytes(msgpack.packb([
"cultcache.store.v1",
[["tests.foreign-id.current", "tests.foreign-id", "tests.foreign_id.v1", "tests.foreign-id.current", "",
["tests.foreign-id.current", "tests.foreign-id.older"], []]],
[["old", "tests.foreign-id.older", "2026-09-30T00:00:00Z", document.encode_payload({"name": "old"})]],
], use_bin_type=True))
before = path.read_bytes()
cache = self._open_foreign_id_store(document, path)
self.assertEqual(cache.get_required_envelope(document, "old").schema_id, "tests.foreign-id.current")
with self.assertRaises(SchemaConflictError) as refused:
cache.put(document, "next", {"name": "next"})
self.assertEqual(refused.exception.schema_id, "tests.foreign-id.older")
self.assertEqual(refused.exception.record_key, "old")
self.assertEqual(path.read_bytes(), before)
# A record resolves to a local document by its schema id, never by the name its catalog entry carries: a schema renamed under
# a stable id opens, and an id that names one document is not overruled by a name that names another.
def _store_of(self, path: Path, catalog: list, records: list) -> None:
import msgpack # type: ignore
path.write_bytes(msgpack.packb(["cultcache.store.v1", catalog, records], use_bin_type=True))
def test_a_schema_renamed_under_a_stable_id_opens_by_its_id(self) -> None:
document = define_database_entry_type(
"tests.old", [("name", 0)], schema_id="tests.stable", schema_name="tests.old", schema_version="tests.old.v1",
)
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.msgpack"
self._store_of(
path,
[["tests.stable", "tests.renamed", "tests.renamed.v1", "renamed", "", ["tests.stable"], []]],
[["a", "tests.stable", "2026-09-30T00:00:00Z", document.encode_payload({"name": "a"})]],
)
cache = self._open_foreign_id_store(document, path)
self.assertEqual(cache.get_required(document, "a")["name"], "a")
def test_a_schema_id_that_names_one_document_is_not_overruled_by_a_name_that_names_another(self) -> None:
first = define_database_entry_type("tests.a", [("name", 0)], schema_id="id.a", schema_name="tests.a", schema_version="tests.a.v1")
second = define_database_entry_type(
"tests.b", [("name", 0)], schema_id="id.b", schema_name="tests.b", schema_version="tests.b.v1", compatible_schema_ids=["id.b", "id.b.older"],
)
with tempfile.TemporaryDirectory() as tmp:
for schema_id in ("id.b", "id.b.older"):
path = Path(tmp) / (schema_id + ".msgpack")
# The entry that publishes the id carries the other document's name.
self._store_of(
path,
[["id.b", "tests.a", "tests.b.v1", "h", "", ["id.b", "id.b.older"], []]],
[["k", schema_id, "2026-09-30T00:00:00Z", second.encode_payload({"name": "k"})]],
)
cache = CultCache.builder().register_document_type(first).register_document_type(second).add_generic_store(
SingleFileMessagePackBackingStore(path)
).build()
cache.pull_all_backing_stores()
self.assertEqual(cache.get_required(second, "k")["name"], "k", schema_id)
self.assertIsNone(cache.get(first, "k"), schema_id)
def _assert_second_claim_refused(self, first, second, schema_id: str) -> None:
"""A registration refusal is typed, names the id and both schema names and document types, and the cache keeps what it
had."""
from cultcache_py import SchemaConflictError
cache = CultCache()
cache.register_document_type(first)
with self.assertRaises(SchemaConflictError) as refused:
cache.register_document_type(second)
self.assertEqual(refused.exception.schema_id, schema_id)
self.assertEqual(refused.exception.schema_names, [first.catalog_entry().schema_name, second.catalog_entry().schema_name])
self.assertIn(first.type, str(refused.exception))
self.assertIn(second.type, str(refused.exception))
if second is not first:
with self.assertRaisesRegex(CultCacheError, "not registered"):
cache.put(second, "k", {"name": "k"})
def _pull(self, documents: list, catalog: list, records: list) -> CultCache:
tmp = tempfile.mkdtemp()
path = Path(tmp) / "store.msgpack"
self._store_of(path, catalog, records)
builder = CultCache.builder()
for document in documents:
builder = builder.register_document_type(document)
cache = builder.add_generic_store(SingleFileMessagePackBackingStore(path)).build()
cache.pull_all_backing_stores()
return cache
def test_a_document_listing_an_id_another_owns_registers_beside_it_in_either_order_and_the_owner_wins(self) -> None:
owner = define_database_entry_type("tests.owner", [("name", 0)], schema_id="id.owned", schema_name="tests.owner", schema_version="tests.owner.v1")
lister = define_database_entry_type(
"tests.lister", [("name", 0)], schema_id="id.lister", schema_name="tests.lister", schema_version="tests.lister.v1",
compatible_schema_ids=["id.lister", "id.owned"],
)
catalog = [
["id.owned", "tests.owner", "tests.owner.v1", "h1", "", ["id.owned"], []],
["id.lister", "tests.lister", "tests.lister.v1", "h2", "", ["id.lister", "id.owned"], []],
]
records = [
["o", "id.owned", "2026-09-30T00:00:00Z", owner.encode_payload({"name": "o"})],
["l", "id.lister", "2026-09-30T00:00:00Z", lister.encode_payload({"name": "l"})],
]
for order in ([owner, lister], [lister, owner]):
cache = self._pull(order, catalog, records)
self.assertEqual(cache.get_required(owner, "o")["name"], "o")
self.assertIsNone(cache.get(lister, "o"))
self.assertEqual(cache.get_required(lister, "l")["name"], "l")
self.assertEqual(self._pull([lister], catalog, records[:1]).get_required(lister, "o")["name"], "o")
def test_two_documents_listing_one_id_register_and_a_record_under_it_needs_an_owner(self) -> None:
from cultcache_py import SchemaConflictError
a = define_database_entry_type(
"tests.list-a", [("name", 0)], schema_id="id.a", schema_name="tests.list-a", compatible_schema_ids=["id.a", "id.shared"],
)
b = define_database_entry_type(
"tests.list-b", [("name", 0)], schema_id="id.b", schema_name="tests.list-b", compatible_schema_ids=["id.b", "id.shared"],
)
owner = define_database_entry_type("tests.shared", [("name", 0)], schema_id="id.shared", schema_name="tests.shared")
catalog = [["id.shared", "tests.shared", "tests.shared.v1", "h", "", ["id.shared"], []]]
records = [["k", "id.shared", "2026-09-30T00:00:00Z", owner.encode_payload({"name": "k"})]]
for listers in ([a, b], [b, a]):
with self.assertRaises(SchemaConflictError) as refused:
self._pull(listers, catalog, records)
self.assertEqual(refused.exception.schema_id, "id.shared")
self.assertEqual(refused.exception.record_key, "k")
self.assertEqual(refused.exception.schema_names, ["tests.list-a", "tests.list-b"])
for documents in ([*listers, owner], [owner, *listers]):
cache = self._pull(documents, catalog, records)
self.assertEqual(cache.get_required(owner, "k")["name"], "k")
def test_two_documents_owning_one_schema_id_are_refused_in_either_order(self) -> None:
# Each declares an older id and not its own, so the id they share is only ever their own.
a = define_database_entry_type("tests.own-a", [("name", 0)], schema_id="id.same", schema_name="tests.own-a", compatible_schema_ids=["id.a-older"])
b = define_database_entry_type("tests.own-b", [("name", 0)], schema_id="id.same", schema_name="tests.own-b", compatible_schema_ids=["id.b-older"])
self._assert_second_claim_refused(a, b, "id.same")
self._assert_second_claim_refused(b, a, "id.same")
def test_every_registration_refusal_is_a_schema_conflict(self) -> None:
first = define_database_entry_type("tests.named", [("name", 0)], schema_id="id.first", schema_name="tests.named")
same_name = define_database_entry_type("tests.renamed", [("name", 0)], schema_id="id.second", schema_name="tests.named")
self._assert_second_claim_refused(first, same_name, "id.second")
self._assert_second_claim_refused(first, first, "id.first")
def test_interop_cli_helpers_round_trip_v1_store(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
store_path = str(Path(tmp) / "cache.msgpack")
written = write_note(store_path, "python-test")
loaded = read_note(store_path)
self.assertEqual(loaded["documentId"], written["documentId"])
self.assertEqual(loaded["authorRuntimeId"], "python-test")
self.assertIn("interop", loaded["tags"])
def test_second_generic_store_rejected(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache = CultCache()
cache.add_generic_store(SingleFileMessagePackBackingStore(Path(tmp) / "a.cc"))
with self.assertRaisesRegex(CultCacheError, "second generic store"):
cache.add_generic_store(SingleFileMessagePackBackingStore(Path(tmp) / "b.cc"))
def test_type_claimed_twice_rejected(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cache = CultCache()
cache.add_backing_store(SingleFileMessagePackBackingStore(Path(tmp) / "a.cc"), ["item"])
with self.assertRaisesRegex(CultCacheError, "already routed"):
cache.add_backing_store(SingleFileMessagePackBackingStore(Path(tmp) / "b.cc"), ["settings", "item"])
# A refused registration claims nothing, so "settings" is still free.
cache.add_backing_store(SingleFileMessagePackBackingStore(Path(tmp) / "c.cc"), ["settings"])
def test_types_route_to_home_store(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
item = item_doc()
settings = define_database_entry_type("settings", [("theme", 0)])
generic_path = Path(tmp) / "generic.cc"
settings_path = Path(tmp) / "settings.cc"
def build() -> CultCache:
return (
CultCache.builder()
.register_document_type(item)
.register_document_type(settings)
.add_backing_store(SingleFileMessagePackBackingStore(settings_path), ["settings"])
.add_generic_store(SingleFileMessagePackBackingStore(generic_path))
.build()
)
cache = build()
cache.put(item, "item:potion", Item(name="Potion", category="Consumable", value=50))
cache.put(settings, "app", {"theme": "ash"})
generic = SingleFileMessagePackBackingStore(generic_path).pull_all()
routed = SingleFileMessagePackBackingStore(settings_path).pull_all()
self.assertEqual([(e.key, e.type) for e in generic], [("item:potion", "item")])
self.assertEqual([(e.key, e.type) for e in routed], [("app", "settings")])
cache.delete(settings, "app")
self.assertEqual(SingleFileMessagePackBackingStore(settings_path).pull_all(), [])
self.assertEqual(len(SingleFileMessagePackBackingStore(generic_path).pull_all()), 1)
reloaded = build()
reloaded.pull_all_backing_stores()
self.assertEqual(reloaded.get_required(item, "item:potion").value, 50)
self.assertIsNone(reloaded.get(settings, "app"))
def test_attach_that_would_move_a_held_record_home_is_refused(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
settings = define_database_entry_type("settings", [("theme", 0)])
generic_path = Path(tmp) / "generic.cc"
settings_path = Path(tmp) / "settings.cc"
cache = (
CultCache.builder()
.register_document_type(settings)
.add_generic_store(SingleFileMessagePackBackingStore(generic_path))
.build()
)
cache.put(settings, "app", {"theme": "v1-generic"})
with self.assertRaisesRegex(
CultCacheError, "would move settings from the generic store to the store routed to settings"
):
cache.add_backing_store(SingleFileMessagePackBackingStore(settings_path), ["settings"])
# Nothing attached: the next write still lands in the generic store.
cache.put(settings, "app", {"theme": "v2-generic"})
generic = SingleFileMessagePackBackingStore(generic_path).pull_all()
self.assertEqual([(e.key, e.type) for e in generic], [("app", "settings")])
self.assertFalse(settings_path.exists())
def test_load_from_a_store_that_is_not_the_record_home_is_refused(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
settings = define_database_entry_type("settings", [("theme", 0)])
generic_path = Path(tmp) / "generic.cc"
settings_path = Path(tmp) / "settings.cc"
writer = CultCache.builder().register_document_type(settings).add_generic_store(
SingleFileMessagePackBackingStore(generic_path)
).build()
writer.put(settings, "app", {"theme": "stray"})
generic_bytes = generic_path.read_bytes()
cache = (
CultCache.builder()
.register_document_type(settings)
.add_backing_store(SingleFileMessagePackBackingStore(settings_path), ["settings"])
.add_generic_store(SingleFileMessagePackBackingStore(generic_path))
.build()
)
# The cache already holds a record, in its home store, before the refused load.
cache.put(settings, "held", {"theme": "home"})
before = cache.snapshot_envelopes()
settings_bytes = settings_path.read_bytes()
with self.assertRaisesRegex(
CultCacheError,
"settings record app was loaded from the generic store, but its home is the store routed to settings",
):
cache.pull_all_backing_stores()
self.assertIsNone(cache.get(settings, "app"))
self.assertEqual(cache.snapshot_envelopes(), before)
self.assertEqual(cache.get_required(settings, "held"), {"theme": "home"})
self.assertEqual(generic_path.read_bytes(), generic_bytes)
self.assertEqual(settings_path.read_bytes(), settings_bytes)
def test_pull_with_duplicate_global_leaves_cache_unchanged(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
item = item_doc()
settings = define_document_type("settings", global_document=True)
store_path = Path(tmp) / "generic.cc"
cache = (
CultCache.builder()
.register_document_type(item)
.register_document_type(settings)
.add_generic_store(SingleFileMessagePackBackingStore(store_path))
.build()
)
cache.put(item, "item:potion", Item(name="Potion", category="Consumable", value=50))
cache.put_global(settings, {"theme": "ash"})
before = cache.snapshot_envelopes()
# A second record of the global type lands in the store file behind the cache's back.
stray = replace(cache.get_required_envelope(settings, CultCache.GLOBAL_KEY), key="stray")
SingleFileMessagePackBackingStore(store_path).push(stray)
with self.assertRaisesRegex(CultCacheError, "Duplicate global document for type: settings"):
cache.pull_all_backing_stores()
self.assertEqual(cache.snapshot_envelopes(), before)
self.assertEqual(cache.get_required(item, "item:potion").value, 50)
self.assertEqual(cache.get_key_by_name(item, "Potion"), "item:potion")
self.assertEqual(cache.get_required_global(settings)["theme"], "ash")
def test_pull_with_throwing_index_accessor_leaves_cache_unchanged(self) -> None:
def category(item: Item) -> str:
if item.name == "Bomb":
raise ValueError("category accessor refuses Bomb")
return item.category
with tempfile.TemporaryDirectory() as tmp:
item = define_document_type(
"item",
encode=lambda value: asdict(value),
decode=lambda raw: Item(**raw),
name="name",
indexes={"category": category},
)
settings = define_document_type("settings", global_document=True)
store_path = Path(tmp) / "generic.cc"
cache = (
CultCache.builder()
.register_document_type(item)
.register_document_type(settings)
.add_generic_store(SingleFileMessagePackBackingStore(store_path))
.build()
)
cache.put(item, "item:potion", Item(name="Potion", category="Consumable", value=50))
cache.put_global(settings, {"theme": "ash"})
before_envelopes = cache.snapshot_envelopes()
before_values = cache.snapshot()
# A record whose index accessor throws lands in the store file behind the cache's back.
bomb = replace(
cache.get_required_envelope(item, "item:potion"),
key="item:bomb",
payload=item.encode_payload(Item(name="Bomb", category="Explosive", value=1)),
)
SingleFileMessagePackBackingStore(store_path).push(bomb)
with self.assertRaisesRegex(ValueError, "category accessor refuses Bomb"):
cache.pull_all_backing_stores()
self.assertEqual(cache.snapshot_envelopes(), before_envelopes)
self.assertEqual(cache.snapshot(), before_values)
self.assertEqual(cache.get_required(item, "item:potion").value, 50)
self.assertIsNone(cache.get(item, "item:bomb"))
self.assertEqual(cache.get_key_by_name(item, "Potion"), "item:potion")
self.assertIsNone(cache.get_key_by_name(item, "Bomb"))
self.assertEqual(cache.get_key_by_index(item, "category", "Consumable"), "item:potion")
self.assertEqual(cache.get_required_global(settings)["theme"], "ash")
def _store_state(self, path: Path) -> list[tuple[str, str, bytes]]:
return sorted((e.type, e.key, e.payload) for e in SingleFileMessagePackBackingStore(path).pull_all())
def test_put_with_throwing_name_extractor_changes_neither_store_nor_cache(self) -> None:
def name(value: dict) -> str:
if value["n"] == "bad":
raise ValueError("extractor refuses bad")
return value["n"]
with tempfile.TemporaryDirectory() as tmp:
item = define_document_type("item", name=name)
path = Path(tmp) / "generic.cc"
cache = CultCache.builder().register_document_type(item).add_generic_store(
SingleFileMessagePackBackingStore(path)
).build()
cache.put(item, "k", {"n": "good"})
store_before, cache_before = self._store_state(path), cache.snapshot_envelopes()
with self.assertRaisesRegex(ValueError, "extractor refuses bad"):
cache.put(item, "k", {"n": "bad"})
self.assertEqual(self._store_state(path), store_before)
self.assertEqual(cache.snapshot_envelopes(), cache_before)
self.assertEqual(cache.get_required(item, "k"), {"n": "good"})
self.assertEqual(cache.get_key_by_name(item, "good"), "k")
def test_overwrite_under_a_registered_extractor_that_now_raises_changes_neither_store_nor_cache(self) -> None:
refusing = {"on": False}
def category(value: dict) -> str:
if refusing["on"]:
raise ValueError("extractor refuses now")
return value["c"]
with tempfile.TemporaryDirectory() as tmp:
item = define_document_type("item")
path = Path(tmp) / "generic.cc"
cache = CultCache.builder().register_document_type(item).add_generic_store(
SingleFileMessagePackBackingStore(path)
).build()
cache.put(item, "k", {"c": "old"})
cache.register_index(item, "category", category)
refusing["on"] = True
store_before, cache_before = self._store_state(path), cache.snapshot_envelopes()
with self.assertRaisesRegex(ValueError, "extractor refuses now"):
cache.put(item, "k", {"c": "new"})
self.assertEqual(self._store_state(path), store_before)
self.assertEqual(cache.snapshot_envelopes(), cache_before)
self.assertEqual(cache.get_key_by_index(item, "category", "old"), "k")
# Deleting runs no extractor: it uses the lookup keys stored when the record was admitted.
cache.delete(item, "k")
self.assertIsNone(cache.get_key_by_index(item, "category", "old"))
def test_registering_an_extractor_that_raises_on_a_held_value_installs_nothing(self) -> None:
def name(value: dict) -> str:
if value["n"] == "bad":
raise ValueError("extractor refuses bad")
return value["n"]
with tempfile.TemporaryDirectory() as tmp:
item = define_document_type("item")
path = Path(tmp) / "generic.cc"
cache = CultCache.builder().register_document_type(item).add_generic_store(
SingleFileMessagePackBackingStore(path)
).build()
cache.put(item, "old", {"n": "bad"})
with self.assertRaisesRegex(ValueError, "extractor refuses bad"):
cache.register_name_lookup(item, name)
cache.put(item, "old", {"n": "fine"})
self.assertEqual(cache.get_required(item, "old"), {"n": "fine"})
self.assertIsNone(cache.get_key_by_name(item, "fine"))
self.assertEqual([item.decode_payload(e.payload) for e in SingleFileMessagePackBackingStore(path).pull_all()], [{"n": "fine"}])
def test_put_envelopes_with_throwing_extractor_changes_neither_store_nor_cache(self) -> None:
def name(value: dict) -> str:
if value["n"] == "bad":
raise ValueError("extractor refuses bad")
return value["n"]
with tempfile.TemporaryDirectory() as tmp:
item = define_document_type("item", name=name)
path = Path(tmp) / "generic.cc"
cache = CultCache.builder().register_document_type(item).add_generic_store(
SingleFileMessagePackBackingStore(path)
).build()
cache.put(item, "seed", {"n": "seed"})
seed = cache.get_required_envelope(item, "seed")
first = replace(seed, key="a", payload=item.encode_payload({"n": "a"}))
second = replace(seed, key="b", payload=item.encode_payload({"n": "bad"}))
store_before, cache_before = self._store_state(path), cache.snapshot_envelopes()
with self.assertRaisesRegex(ValueError, "extractor refuses bad"):
cache.put_envelopes(item, [first, second])
self.assertEqual(self._store_state(path), store_before)
self.assertEqual(cache.snapshot_envelopes(), cache_before)
self.assertIsNone(cache.get_key_by_name(item, "a"))
def test_global_replace_with_failing_store_push_keeps_old_global(self) -> None:
class FailingPushStore(SingleFileMessagePackBackingStore):
fail = False
def push(self, envelope):
if self.fail:
raise OSError("disk full")
super().push(envelope)
with tempfile.TemporaryDirectory() as tmp:
settings = define_document_type("settings", global_document=True)
path = Path(tmp) / "generic.cc"
store = FailingPushStore(path)
cache = CultCache.builder().register_document_type(settings).add_generic_store(store).build()
cache.put_global(settings, {"theme": "base"})
store_before, cache_before = self._store_state(path), cache.snapshot_envelopes()
store.fail = True
with self.assertRaisesRegex(OSError, "disk full"):
cache.put_global(settings, {"theme": "new"})
with self.assertRaisesRegex(CultCacheError, "must use key __global__"):
cache.put(settings, "other", {"theme": "new"})
self.assertEqual(self._store_state(path), store_before)
self.assertEqual(cache.snapshot_envelopes(), cache_before)
self.assertEqual(cache.get_required_global(settings), {"theme": "base"})
@staticmethod
def _pausing_store(path: Path, barrier: threading.Barrier, resume: threading.Event):
"""A store whose first push writes, meets the other thread at the barrier, then waits until that
thread's call has returned (or a timeout: a locked cache makes the other thread wait on us)."""
class PausingStore(SingleFileMessagePackBackingStore):
pushes = 0
paused = False
other_reached_store_while_paused = False
def push(self, envelope):
self.other_reached_store_while_paused |= self.paused
self.pushes += 1
super().push(envelope)
if self.pushes == 1:
self.paused = True
barrier.wait(timeout=5)
resume.wait(timeout=0.5)
self.paused = False
return PausingStore(path)
@staticmethod
def _run_racing(first, second, resume: threading.Event) -> list[BaseException]:
errors: list[BaseException] = []
def run(action, signal):
try:
action()
except BaseException as error: # noqa: BLE001 - reported to the test
errors.append(error)
finally:
if signal:
resume.set()
threads = [threading.Thread(target=run, args=(first, False)), threading.Thread(target=run, args=(second, True))]
for thread in threads:
thread.start()
for thread in threads:
thread.join(timeout=10)
return errors
def test_concurrent_put_and_attach_cannot_land_record_in_non_home_store(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
settings = define_database_entry_type("settings", [("theme", 0)])
generic_path, settings_path = Path(tmp) / "generic.cc", Path(tmp) / "settings.cc"
barrier, resume = threading.Barrier(2), threading.Event()
generic = self._pausing_store(generic_path, barrier, resume)
cache = CultCache.builder().register_document_type(settings).add_generic_store(generic).build()
def attach() -> None:
barrier.wait(timeout=5)
cache.add_backing_store(SingleFileMessagePackBackingStore(settings_path), ["settings"])
errors = self._run_racing(lambda: cache.put(settings, "app", {"theme": "t"}), attach, resume)
# Either the attach was refused (the record is held, so its home cannot move) or it won;
# in every case each held record must be in the store that is its home.
home = SingleFileMessagePackBackingStore(generic_path if errors else settings_path)
self.assertEqual([(e.key, e.type) for e in home.pull_all()], [("app", "settings")])
self.assertTrue(all(isinstance(error, CultCacheError) for error in errors), errors)
def test_concurrent_global_puts_cannot_both_reach_disk(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
settings = define_document_type("settings", global_document=True)
path = Path(tmp) / "generic.cc"
barrier, resume = threading.Barrier(2), threading.Event()
store = self._pausing_store(path, barrier, resume)
cache = CultCache.builder().register_document_type(settings).add_generic_store(store).build()
def second() -> None:
barrier.wait(timeout=5)
cache.put_global(settings, {"theme": "second"})
errors = self._run_racing(lambda: cache.put_global(settings, {"theme": "first"}), second, resume)
self.assertEqual(errors, [])
# While the first write was between its store push and its apply, the second never reached disk.
self.assertFalse(store.other_reached_store_while_paused)
on_disk = SingleFileMessagePackBackingStore(path).pull_all()
self.assertEqual(len(on_disk), 1)
self.assertEqual(on_disk[0].payload, cache.get_required_envelope(settings, CultCache.GLOBAL_KEY).payload)
self.assertEqual(cache.get_required_global(settings), {"theme": "second"})
@staticmethod
def _envelope(document, key: str, value) -> CultCacheEnvelope:
catalog_entry = document.catalog_entry()
return CultCacheEnvelope.create(
key=key,
type=document.type,
payload=document.encode_payload(value),
schema_id=catalog_entry.schema_id,
catalog_entry=catalog_entry,
)
def test_legacy_key_global_loads_without_writing_and_first_write_leaves_one_global(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
settings = define_document_type("settings", global_document=True)
path = Path(tmp) / "generic.cc"
def build() -> CultCache:
return CultCache.builder().register_document_type(settings).add_generic_store(
SingleFileMessagePackBackingStore(path)
).build()
SingleFileMessagePackBackingStore(path).push(self._envelope(settings, "legacy", {"theme": "old"}))
stored_bytes = path.read_bytes()
cache = build()
cache.pull_all_backing_stores()
self.assertEqual(cache.get_global(settings), {"theme": "old"})
self.assertEqual(cache.get(settings, CultCache.GLOBAL_KEY), {"theme": "old"})
self.assertEqual(path.read_bytes(), stored_bytes)
cache.put_global(settings, {"theme": "new"})
self.assertEqual([e.key for e in SingleFileMessagePackBackingStore(path).pull_all()], [CultCache.GLOBAL_KEY])
reloaded = build()
reloaded.pull_all_backing_stores()
self.assertEqual(reloaded.get_required_global(settings), {"theme": "new"})
def test_deleting_a_legacy_key_global_removes_the_legacy_record(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
settings = define_document_type("settings", global_document=True)
path = Path(tmp) / "generic.cc"
SingleFileMessagePackBackingStore(path).push(self._envelope(settings, "legacy", {"theme": "old"}))
cache = CultCache.builder().register_document_type(settings).add_generic_store(
SingleFileMessagePackBackingStore(path)
).build()
cache.pull_all_backing_stores()
cache.delete_global(settings)
self.assertEqual(SingleFileMessagePackBackingStore(path).pull_all(), [])
self.assertIsNone(cache.get_global(settings))
def test_two_globals_of_one_type_on_disk_are_refused_under_any_keys(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
settings = define_document_type("settings", global_document=True)
for keys in (["one", "two"], ["legacy", CultCache.GLOBAL_KEY]):
path = Path(tmp) / f"{keys[0]}.cc"
store = SingleFileMessagePackBackingStore(path)
store.push_all([self._envelope(settings, key, {"theme": key}) for key in keys])
cache = CultCache.builder().register_document_type(settings).add_generic_store(store).build()
with self.assertRaisesRegex(CultCacheError, "Duplicate global document for type: settings"):
cache.pull_all_backing_stores()
self.assertIsNone(cache.get_global(settings))
def test_decoder_writing_during_pull_raises_and_changes_neither_store_nor_cache(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "generic.cc"
holder: dict[str, CultCache] = {}
def decode(raw):
holder["cache"].put(item, "side", {"name": "side"})
return raw
item = define_document_type("item", decode=decode, encode=lambda value: value)
store = SingleFileMessagePackBackingStore(path)
store.push(self._envelope(item, "a", {"name": "a"}))
stored_bytes = path.read_bytes()
cache = holder["cache"] = CultCache.builder().register_document_type(item).add_generic_store(store).build()
with self.assertRaisesRegex(CultCacheError, "re-entrant put"):
cache.pull_all_backing_stores()
self.assertEqual(path.read_bytes(), stored_bytes)
self.assertEqual(cache.snapshot_envelopes(), [])
self.assertIsNone(cache.get(item, "side"))
# The refusal released the cache: an ordinary write still works.
cache.put(item, "b", {"name": "b"})
self.assertEqual(cache.get(item, "b"), {"name": "b"})
def test_index_extractor_writing_during_register_index_raises_and_installs_nothing(self) -> None:
thing = define_document_type("thing")
cache = CultCache.builder().register_document_type(thing).build()
cache.put(thing, "k1", {"n": "one", "cat": "x"})
def category(value: dict) -> str:
cache.put(thing, "k2", {"n": "two", "cat": "y"})
return value["cat"]
with self.assertRaisesRegex(CultCacheError, "re-entrant put"):
cache.register_index(thing, "cat", category)
self.assertIsNone(cache.get(thing, "k2"))
self.assertNotIn("thing", cache._state.index_extractors)
self.assertIsNone(cache.get_key_by_index(thing, "cat", "x"))
def test_put_envelope_refuses_a_global_under_another_key(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
settings = define_document_type("settings", global_document=True)
path = Path(tmp) / "generic.cc"
cache = CultCache.builder().register_document_type(settings).add_generic_store(
SingleFileMessagePackBackingStore(path)
).build()
cache.put_global(settings, {"theme": "base"})
store_before, cache_before = self._store_state(path), cache.snapshot_envelopes()
stray = replace(cache.get_required_envelope(settings, CultCache.GLOBAL_KEY), key="stray")
with self.assertRaisesRegex(CultCacheError, "must use key __global__"):
cache.put_envelope(settings, stray)
with self.assertRaisesRegex(CultCacheError, "must be non-empty"):
cache.put_envelope(settings, replace(stray, key=""))
self.assertEqual(self._store_state(path), store_before)
self.assertEqual(cache.snapshot_envelopes(), cache_before)
def test_refused_put_leaves_no_empty_type_entry(self) -> None:
def refuse(value: dict) -> dict:
raise ValueError("encode refuses")
with tempfile.TemporaryDirectory() as tmp:
other = define_document_type("other", encode=refuse)
path = Path(tmp) / "generic.cc"
cache = CultCache.builder().register_document_type(other).add_generic_store(
SingleFileMessagePackBackingStore(path)
).build()
with self.assertRaisesRegex(ValueError, "encode refuses"):
cache.put(other, "x", {"a": 1})
self.assertEqual(cache.snapshot(), {})
self.assertFalse(path.exists())
def test_attach_with_empty_type_list_is_the_generic_store(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
settings = define_database_entry_type("settings", [("theme", 0)])
store_path = Path(tmp) / "generic.cc"
cache = CultCache.builder().register_document_type(settings).build()
cache.add_backing_store(SingleFileMessagePackBackingStore(store_path), [])
cache.put(settings, "y", {"theme": "t"})
self.assertEqual(
[(e.key, e.type) for e in SingleFileMessagePackBackingStore(store_path).pull_all()],
[("y", "settings")],
)
with self.assertRaisesRegex(CultCacheError, "second generic store"):
cache.add_backing_store(SingleFileMessagePackBackingStore(Path(tmp) / "b.cc"), [])
def test_zero_store_cache_writes_and_deletes_in_memory(self) -> None:
settings = define_database_entry_type("settings", [("theme", 0)])
cache = CultCache.builder().register_document_type(settings).build()
cache.put(settings, "x", {"theme": "mem"})
self.assertEqual(cache.get_required(settings, "x"), {"theme": "mem"})
cache.delete(settings, "x")
self.assertIsNone(cache.get(settings, "x"))
def test_cache_put_without_backing_store_keeps_in_memory_value(self) -> None:
document = define_database_entry_type(
"bench.memory_item",
[
("name", 0),
("value", 1, 0),
],
)
cache = CultCache()
cache.register_document_type(document)
cache.put(document, "item:one", {"name": "one", "value": 1})
self.assertEqual(cache.get(document, "item:one"), {"name": "one", "value": 1})
self.assertEqual(cache.get_required_envelope(document, "item:one").key, "item:one")
# Shared refusal vectors: tests/vectors/document-variants-c0, read by every runtime's tests.
_VECTORS = Path(__file__).resolve().parents[3] / "tests" / "vectors" / "document-variants-c0"
_ITEM_SCHEMA_ID = "sha256:88d3fdf0a927acf3b163940d8f8c7fe62b3316542ce771a67ec8bc038f594788"
def _pull_vector(self, name: str) -> list[CultCacheEnvelope]:
with tempfile.TemporaryDirectory() as tmp:
store_path = Path(tmp) / "store.msgpack"
store_path.write_bytes((self._VECTORS / name).read_bytes())
return SingleFileMessagePackBackingStore(store_path).pull_all()
def test_single_file_refuses_unknown_header_by_name(self) -> None:
with self.assertRaises(ValueError) as caught:
self._pull_vector("unknown-header.msgpack")
self.assertIn("cultcache.store.v9", str(caught.exception))
def test_single_file_refuses_extra_record_slot_naming_the_record(self) -> None:
with self.assertRaises(ValueError) as caught:
self._pull_vector("extra-slot-full-payload.msgpack")
self.assertIn("item:anvil", str(caught.exception))
self.assertIn(self._ITEM_SCHEMA_ID, str(caught.exception))
def test_single_file_refuses_variant_store_by_version_or_record(self) -> None:
with self.assertRaises(ValueError) as caught:
self._pull_vector("variant-v2.msgpack")
message = str(caught.exception)
self.assertTrue("cultcache.store.v2" in message or "item:anvil-big" in message, message)
def test_single_file_refuses_the_csharp_written_variant_store_by_version_or_record(self) -> None:
with self.assertRaises(ValueError) as caught:
self._pull_vector("../document-variants-c1/variant-store.msgpack")
message = str(caught.exception)
self.assertTrue("cultcache.store.v2" in message or "laser-big" in message, message)
def test_v1_store_written_at_the_base_commit_still_reads_byte_for_byte(self) -> None:
envelopes = self._pull_vector("v1-base.msgpack")
self.assertEqual([(e.key, e.type) for e in envelopes], [("alpha", "vectors.item"), ("beta", "vectors.item")])
self.assertEqual(envelopes[0].payload, b"\x92\xa5alpha\x01")
self.assertEqual(envelopes[1].payload, b"\x92\xa4beta\x02")
def test_single_file_refusals_say_what_was_found_without_blaming_variants(self) -> None:
with self.assertRaises(ValueError) as unsupported:
self._pull_vector("unknown-header.msgpack")
self.assertIn("not one this runtime reads", str(unsupported.exception))
self.assertNotIn("variant", str(unsupported.exception))
with self.assertRaises(ValueError) as invalid:
self._pull_vector("extra-slot-full-payload.msgpack")
self.assertIn("not a valid store", str(invalid.exception))
self.assertNotIn("variant", str(invalid.exception))
def test_single_file_only_a_missing_store_reads_as_empty(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
self.assertEqual(SingleFileMessagePackBackingStore(Path(tmp) / "store.cc").pull_all(), [])
# An existing empty file is not a store: no CultCache writer leaves one.
empty = Path(tmp) / "empty.cc"
empty.write_bytes(b"")
with self.assertRaises(ValueError):
SingleFileMessagePackBackingStore(empty).pull_all()
self.assertEqual(empty.read_bytes(), b"")
# An empty array is the legacy envelope array with no envelopes: no header, no format claimed.
legacy_empty = Path(tmp) / "legacy-empty.cc"
legacy_empty.write_bytes(b"\x90")
self.assertEqual(SingleFileMessagePackBackingStore(legacy_empty).pull_all(), [])
def test_single_file_refusals_never_echo_a_value_from_the_store(self) -> None:
import msgpack # type: ignore
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.cc"
cases = [
(["cultcache.store.v12", [], []], True),
(["cultcache.store.SECRET-HEADER", [], []], False),
(["cultcache.store.v12SECRET", [], []], False),
(["cultcache.store.v", [], []], False),
(["cultcache.store.v1", ["SECRET-CATALOG-TEXT"], []], None),
]
for value, echoed in cases:
path.write_bytes(msgpack.packb(value, use_bin_type=True))
with self.assertRaises(ValueError) as caught:
SingleFileMessagePackBackingStore(path).pull_all()
message = str(caught.exception)
if echoed:
self.assertIn(repr(value[0]), message)
else:
self.assertNotIn("SECRET", message)
if echoed is False:
self.assertNotIn(repr(value[0]), message)
self.assertIn(f"of {len(value[0])} bytes", message)
def test_single_file_refusals_say_exactly_what_was_found_and_what_is_read(self) -> None:
import msgpack # type: ignore
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.cc"
reads = "it reads 'cultcache.store.v1' and 'cultcache.store.v3' only"
for header, found in (
("cultcache.store.v12", "'cultcache.store.v12'"),
("cultcache.store.SECRET", "an unrecognised cultcache.store.* header of 22 bytes"),
("cultcache.store.v", "an unrecognised cultcache.store.* header of 17 bytes"),
# Digits are ASCII only: two Arabic-Indic digits are four bytes and are not the known shape.
("cultcache.store.v١٢", "an unrecognised cultcache.store.* header of 21 bytes"),
):
path.write_bytes(msgpack.packb([header, [], []], use_bin_type=True))
with self.assertRaises(ValueError) as caught:
SingleFileMessagePackBackingStore(path).pull_all()
self.assertIn(f"CultCache store format {found} is not one this runtime reads; {reads}", str(caught.exception))
path.write_bytes(msgpack.packb(["cultcache.store.v1", [], [["k", "s", "t", b"\x90", "extra"]]], use_bin_type=True))
with self.assertRaises(ValueError) as caught:
SingleFileMessagePackBackingStore(path).pull_all()
self.assertIn(
"CultCache record 'k' (schema 's') has 5 slots, more than the 4 of a cultcache.store.v1 record, so this is not a valid store",
str(caught.exception),
)
def test_single_file_record_refusal_names_only_a_string_key(self) -> None:
import msgpack # type: ignore
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "store.cc"
for key, schema in ((["SECRET-IN-LIST"], "s"), (b"SECRET-BYTES", "s"), ("k", {"SECRET": 1})):
path.write_bytes(msgpack.packb(
["cultcache.store.v1", [], [[key, schema, "t", b"\x90", "extra"]]], use_bin_type=True))
with self.assertRaises(ValueError) as caught:
SingleFileMessagePackBackingStore(path).pull_all()
self.assertNotIn("SECRET", str(caught.exception))
self.assertIn("not a valid store", str(caught.exception))
# What a write does through a symbolic link today. R3 decides whether a write resolves
# the link or refuses it; this pins the current behaviour so that change is deliberate.
@unittest.skipIf(os.name != "posix", "symbolic links need POSIX here")
def test_r3_decides_resolve_or_refuse_a_push_through_a_live_link_replaces_the_link(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
volume = Path(tmp) / "volume"
volume.mkdir()
target = volume / "store.cc"
envelope = CultCacheEnvelope(key="a", type="t", payload=b"\x90", stored_at="2026-09-30T00:00:00Z")
SingleFileMessagePackBackingStore(target).push(envelope)
before = target.read_bytes()
link = Path(tmp) / "store.cc"
link.symlink_to(target)
SingleFileMessagePackBackingStore(link).push(replace(envelope, key="b"))
self.assertFalse(link.is_symlink())
self.assertTrue(link.is_file())
self.assertEqual(target.read_bytes(), before)
@unittest.skipIf(os.name != "posix", "symbolic links need POSIX here")
def test_a_dangling_symlink_at_a_store_path_is_an_os_error_and_nothing_is_written(self) -> None:
for store_type in (SingleFileMessagePackBackingStore, JsonLinesBackingStore):
with tempfile.TemporaryDirectory() as tmp:
volume = Path(tmp) / "volume"
volume.mkdir()
path = Path(tmp) / "store.cc"
path.symlink_to(volume / "store.cc")
store = store_type(path)
with self.assertRaises(FileNotFoundError):
store.pull_all()
with self.assertRaises(FileNotFoundError):
store.push(CultCacheEnvelope(key="k", type="t", payload=b"\x90", stored_at="2026-09-30T00:00:00Z"))
self.assertTrue(path.is_symlink())
self.assertEqual(list(volume.iterdir()), [])
@unittest.skipIf(os.name != "posix", "symlink loops and file-as-parent errors are POSIX errno cases")
def test_json_lines_store_that_cannot_be_reached_is_an_os_error(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
self.assertEqual(JsonLinesBackingStore(Path(tmp) / "missing.jsonl").pull_all(), [])
loop = Path(tmp) / "loop.jsonl"
loop.symlink_to(loop)
parent = Path(tmp) / "body"
parent.write_bytes(b"not a directory")
for path in (loop, parent / "store.jsonl"):
with self.assertRaises(OSError) as caught:
JsonLinesBackingStore(path).pull_all()
self.assertNotIsInstance(caught.exception, FileNotFoundError)
@unittest.skipIf(os.name != "posix", "symlink loops and file-as-parent errors are POSIX errno cases")
def test_single_file_store_that_cannot_be_reached_is_an_os_error(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
loop = Path(tmp) / "loop.cc"
loop.symlink_to(loop)
parent = Path(tmp) / "body"
parent.write_bytes(b"not a directory")
for path in (loop, parent / "store.cc"):
with self.assertRaises(OSError) as caught:
SingleFileMessagePackBackingStore(path).pull_all()
self.assertNotIsInstance(caught.exception, FileNotFoundError)
# The element-id marker: tests/vectors/document-variants-c2a/v3-base.msgpack is v1-base with its header replaced.
_C2A_VECTORS = Path(__file__).resolve().parents[3] / "tests" / "vectors" / "document-variants-c2a"
def _header_after_rewrite(self, vector: Path) -> str:
import msgpack
with tempfile.TemporaryDirectory() as tmp:
store_path = Path(tmp) / "store.msgpack"
store_path.write_bytes(vector.read_bytes())
store = SingleFileMessagePackBackingStore(store_path)
envelopes = store.pull_all()
self.assertEqual([e.key for e in envelopes], ["alpha", "beta"])
store.push(envelopes[0])
return msgpack.unpackb(store_path.read_bytes(), raw=False)[0]
def test_single_file_reads_a_v3_element_id_store_and_a_rewrite_keeps_the_marker(self) -> None:
self.assertEqual(self._header_after_rewrite(self._C2A_VECTORS / "v3-base.msgpack"), "cultcache.store.v3")
def test_single_file_rewrite_of_a_v1_store_stays_v1(self) -> None:
self.assertEqual(self._header_after_rewrite(self._VECTORS / "v1-base.msgpack"), "cultcache.store.v1")
def test_single_file_flush_after_the_marked_file_is_gone_writes_unmarked(self) -> None:
import msgpack
with tempfile.TemporaryDirectory() as tmp:
store_path = Path(tmp) / "store.msgpack"
store_path.write_bytes((self._C2A_VECTORS / "v3-base.msgpack").read_bytes())
store = SingleFileMessagePackBackingStore(store_path)
envelopes = store.pull_all()
store_path.unlink()
store.push_all(envelopes)
self.assertEqual(msgpack.unpackb(store_path.read_bytes(), raw=False)[0], "cultcache.store.v1")
def test_single_file_push_after_the_marked_file_is_gone_writes_unmarked(self) -> None:
import msgpack
with tempfile.TemporaryDirectory() as tmp:
store_path = Path(tmp) / "store.msgpack"
store_path.write_bytes((self._C2A_VECTORS / "v3-base.msgpack").read_bytes())
store = SingleFileMessagePackBackingStore(store_path)
envelopes = store.pull_all()
store_path.unlink()
store.push(envelopes[0])
self.assertEqual(msgpack.unpackb(store_path.read_bytes(), raw=False)[0], "cultcache.store.v1")
def test_single_file_push_all_refuses_a_store_whose_header_it_cannot_read_and_leaves_it_untouched(self) -> None:
envelopes = SingleFileMessagePackBackingStore(self._C2A_VECTORS / "v3-base.msgpack").pull_all()
vectors = self._VECTORS.parent
for vector in (
self._VECTORS / "unknown-header.msgpack",
self._VECTORS / "variant-v2.msgpack",
vectors / "document-variants-c1" / "variant-store.msgpack",
):
with tempfile.TemporaryDirectory() as tmp:
store_path = Path(tmp) / "store.msgpack"
store_path.write_bytes(vector.read_bytes())
before = store_path.read_bytes()
with self.assertRaisesRegex(StoreUnreadableError, "is not readable"):
SingleFileMessagePackBackingStore(store_path).push_all(envelopes)
self.assertEqual(store_path.read_bytes(), before, f"{vector.name} was rewritten")
# A file is replaced by a rewrite exactly when this runtime's own reader opens it: one verdict per file, asked by
# pull_all, push_all, push and delete alike. The bytes and every runtime's verdict:
# tests/vectors/document-variants-c2a/readability.
def test_single_file_replaces_a_file_exactly_when_it_reads(self) -> None:
import msgpack # type: ignore
root = self._C2A_VECTORS / "readability"
rows = [
line.split()
for line in (root / "manifest.txt").read_text(encoding="utf-8").splitlines()
if line and not line.startswith("#")
]
# Every vector in the folder has a manifest row.
self.assertEqual(
sorted(path.name for path in root.glob("*.bin")),
sorted(vector for vector, *_ in rows if not vector.startswith("..")),
)
envelopes = SingleFileMessagePackBackingStore(self._C2A_VECTORS / "v3-base.msgpack").pull_all()
for vector, _, _, _, python in rows:
content = (root / vector).read_bytes()
for operation in ("pull_all", "push_all", "push", "delete"):
with tempfile.TemporaryDirectory() as tmp:
store_path = Path(tmp) / "store.msgpack"
store_path.write_bytes(content)
store = SingleFileMessagePackBackingStore(store_path)
run = {
"pull_all": store.pull_all,
"push_all": lambda: store.push_all(envelopes),
"push": lambda: store.push(envelopes[0]),
"delete": lambda: store.delete(envelopes[0].type, envelopes[0].key),
}[operation]
if python == "reads":
run()
if operation == "pull_all":
continue
# The replaced file is a store: it carries the header its old content decides (a v3 store keeps its
# marker, all else is v1), and it holds what the operation wrote.
expected = "cultcache.store.v3" if "v3" in vector else "cultcache.store.v1"
self.assertEqual(msgpack.unpackb(store_path.read_bytes(), raw=False)[0], expected, f"{vector} {operation}")
keys = [envelope.key for envelope in SingleFileMessagePackBackingStore(store_path).pull_all()]
if operation == "push_all":
self.assertEqual(sorted(set(keys) & {e.key for e in envelopes}), sorted(e.key for e in envelopes), f"{vector} {operation}")
elif operation == "push":
self.assertIn(envelopes[0].key, keys, f"{vector} {operation}")
else:
self.assertNotIn(envelopes[0].key, keys, f"{vector} {operation}")
else:
with self.assertRaises(StoreUnreadableError, msg=f"{vector} {operation}") as refused:
run()
self.assertIsNotNone(refused.exception.__cause__, f"{vector} {operation}")
self.assertEqual(store_path.read_bytes(), content, f"{vector} {operation} rewrote a file it cannot read")
if __name__ == "__main__":
unittest.main()