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.
117 lines
3.4 KiB
Python
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
|