Files
kanta/tests/test_validate.py
T
LeoVasanko d33f3f9c2f Add @kanta.validate integrity-validation callbacks
Validators receive the live data object (and optionally the Kanta
instance) and raise on inconsistency. They run after replay/migrations
during open() and after each transaction before the change is queued;
a failure rolls back the transaction or aborts the open. Multiple
validators run in registration order until the first failure. Sync-only,
since transactions are synchronous.
2026-09-02 17:02:14 +00:00

117 lines
3.4 KiB
Python

"""Tests for the @kanta.validate integrity-validation callbacks."""
import pytest
from tests.support import Data, make_kanta
pytestmark = pytest.mark.asyncio
async def test_validate_passes_on_valid_data(tmp_path, format_config):
kanta = make_kanta(tmp_path / "d.kantadb", Data, format_config)
calls = []
@kanta.validate
def check(data: Data):
calls.append(data.counter)
assert data.counter >= 0
async with kanta:
with kanta.transaction("inc", log=False) as data:
data.counter = 1
assert calls # ran during bootstrap/open and the transaction
async def test_validate_failure_rolls_back_transaction(tmp_path, format_config):
kanta = make_kanta(tmp_path / "d.kantadb", Data, format_config)
@kanta.validate
def check(data: Data):
if data.counter < 0:
raise ValueError("counter must not go negative")
await kanta.open(log=False)
with pytest.raises(ValueError, match="negative"):
with kanta.transaction("dec", log=False) as data:
data.counter = -1
assert kanta.data.counter == 0 # rolled back
await kanta.close()
# The invalid change never reached the history.
kanta2 = make_kanta(tmp_path / "d.kantadb", Data, format_config)
async with kanta2:
assert kanta2.data.counter == 0
async def test_validate_runs_on_open_after_replay(tmp_path, format_config):
kanta = make_kanta(tmp_path / "d.kantadb", Data, format_config)
async with kanta:
with kanta.transaction("set", log=False) as data:
data.counter = 5
kanta2 = make_kanta(tmp_path / "d.kantadb", Data, format_config)
seen = []
@kanta2.validate
def check(data: Data):
seen.append(data.counter)
async with kanta2:
pass
assert 5 in seen
async def test_validate_failure_aborts_open(tmp_path, format_config):
kanta = make_kanta(tmp_path / "d.kantadb", Data, format_config)
async with kanta:
with kanta.transaction("set", log=False) as data:
data.counter = 5
kanta2 = make_kanta(tmp_path / "d.kantadb", Data, format_config)
@kanta2.validate
def check(data: Data):
raise ValueError("always inconsistent")
with pytest.raises(ValueError, match="inconsistent"):
await kanta2.open(log=False)
# The failed open released the file: a fresh instance can open it.
kanta3 = make_kanta(tmp_path / "d.kantadb", Data, format_config)
async with kanta3:
assert kanta3.data.counter == 5
async def test_multiple_validators_stop_at_first_failure(tmp_path, format_config):
kanta = make_kanta(tmp_path / "d.kantadb", Data, format_config)
calls = []
@kanta.validate
def first(data: Data):
calls.append("first")
if data.counter > 1:
raise ValueError("too big")
@kanta.validate
def second(data: Data):
calls.append("second")
await kanta.open(log=False)
calls.clear()
with pytest.raises(ValueError, match="too big"):
with kanta.transaction("bump", log=False) as data:
data.counter = 2
assert calls == ["first"]
await kanta.close()
async def test_validate_rejects_async_callback(tmp_path, format_config):
kanta = make_kanta(tmp_path / "d.kantadb", Data, format_config)
with pytest.raises(TypeError, match="must not be async"):
@kanta.validate
async def check(data: Data):
pass