mirror of
https://github.com/element-hq/synapse.git
synced 2026-11-04 17:47:28 +00:00
`_calculate_state`, the `use_state_after` roll-back over state deltas of the full-state path, and the delta loop of the incremental path. The database reads that produce their inputs stay with the store. The two Python loops were inline in the handler; they are now functions, `_roll_back_state_over_deltas` and `_apply_current_state_deltas`, so that `tests/sync/test_rust_state_delta.py` can compare them with the Rust on random state maps and deltas. Behaviour is unchanged. One deliberate difference: when the start and end of the timeline disagree on a key that is in neither the timeline nor the previous state, `_calculate_state` sends whichever event its set iteration visits last. The Rust sends the one from the end of the timeline, the state the client should end up with. The test allows the Python either. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0194eNQP5weBtVMg8NKtc6VU
137 lines
4.9 KiB
Python
137 lines
4.9 KiB
Python
#
|
|
# This file is licensed under the Affero General Public License (AGPL) version 3.
|
|
#
|
|
# Copyright (C) 2026 Element Creations Ltd
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# See the GNU Affero General Public License for more details:
|
|
# <https://www.gnu.org/licenses/agpl-3.0.html>.
|
|
#
|
|
|
|
"""The state delta arithmetic of `synapse.handlers.sync` against
|
|
`rust/src/sync/state_delta.rs`, on random state maps and deltas."""
|
|
|
|
import random
|
|
|
|
from parameterized import parameterized
|
|
|
|
from synapse.api.constants import EventTypes
|
|
from synapse.handlers.sync import (
|
|
_apply_current_state_deltas,
|
|
_calculate_state,
|
|
_roll_back_state_over_deltas,
|
|
)
|
|
from synapse.storage.databases.main.state_deltas import StateDelta
|
|
from synapse.synapse_rust.sync import (
|
|
apply_state_deltas_for_test,
|
|
calculate_state_for_test,
|
|
roll_back_state_for_test,
|
|
)
|
|
from synapse.types import (
|
|
StateKey,
|
|
StateMap,
|
|
)
|
|
|
|
from tests import unittest
|
|
from tests.sync.fixtures import (
|
|
ME,
|
|
SEEDS,
|
|
)
|
|
|
|
KEYS: list[StateKey] = [
|
|
(EventTypes.Create, ""),
|
|
(EventTypes.Topic, ""),
|
|
(EventTypes.Name, ""),
|
|
(EventTypes.Member, ME),
|
|
(EventTypes.Member, "@alice:test"),
|
|
(EventTypes.Member, "@bob:test"),
|
|
]
|
|
|
|
|
|
def _random_state_map(rng: random.Random, event_ids: list[str]) -> StateMap[str]:
|
|
"""A random state map over `KEYS`. Event IDs are shared across maps, as
|
|
they are between the state at different points of one room, so the
|
|
same event ID always carries the same key."""
|
|
return {
|
|
key: event_ids[KEYS.index(key) * 3 + rng.randint(0, 2)]
|
|
for key in rng.sample(KEYS, rng.randint(0, len(KEYS)))
|
|
}
|
|
|
|
|
|
class StateDeltaTestCase(unittest.TestCase):
|
|
"""The state delta arithmetic of `synapse.handlers.sync` against
|
|
`rust/src/sync/state_delta.rs`."""
|
|
|
|
EVENT_IDS = [f"${i}" for i in range(len(KEYS) * 3)]
|
|
|
|
@parameterized.expand(SEEDS)
|
|
def test_random_calculate_state_agrees(self, seed: int) -> None:
|
|
rng = random.Random(seed)
|
|
for _ in range(100):
|
|
contains, start, end, previous = (
|
|
_random_state_map(rng, self.EVENT_IDS) for _ in range(4)
|
|
)
|
|
lazy = rng.choice([True, False])
|
|
context = f"maps={(contains, start, end, previous)} lazy={lazy}"
|
|
expected = _calculate_state(contains, start, end, previous, lazy)
|
|
actual = calculate_state_for_test(contains, start, end, previous, lazy)
|
|
self.assertEqual(set(actual), set(expected), context)
|
|
for key, event_id in actual.items():
|
|
if event_id == expected[key]:
|
|
continue
|
|
# The start and end of the timeline disagree on this key.
|
|
# Python sends whichever its set iteration visits last; the
|
|
# Rust sends the end's.
|
|
self.assertEqual(event_id, end[key], context)
|
|
self.assertEqual(expected[key], start[key], context)
|
|
|
|
@staticmethod
|
|
def _random_deltas(rng: random.Random, event_ids: list[str]) -> list[StateDelta]:
|
|
deltas = []
|
|
for i in range(rng.randint(0, 8)):
|
|
key = rng.choice(KEYS)
|
|
deltas.append(
|
|
StateDelta(
|
|
stream_id=i,
|
|
room_id="!a:test",
|
|
event_type=key[0],
|
|
state_key=key[1],
|
|
event_id=rng.choice([None, rng.choice(event_ids)]),
|
|
prev_event_id=rng.choice([None, rng.choice(event_ids)]),
|
|
)
|
|
)
|
|
return deltas
|
|
|
|
@parameterized.expand(SEEDS)
|
|
def test_random_delta_loops_agree(self, seed: int) -> None:
|
|
rng = random.Random(seed)
|
|
for _ in range(100):
|
|
state = _random_state_map(rng, self.EVENT_IDS)
|
|
deltas = self._random_deltas(rng, self.EVENT_IDS)
|
|
rust_deltas = [
|
|
(d.event_type, d.state_key, d.event_id, d.prev_event_id) for d in deltas
|
|
]
|
|
context = f"state={state} deltas={rust_deltas}"
|
|
self.assertEqual(
|
|
roll_back_state_for_test(state, rust_deltas),
|
|
_roll_back_state_over_deltas(state, deltas),
|
|
context,
|
|
)
|
|
|
|
timeline_state = _random_state_map(rng, self.EVENT_IDS)
|
|
initial = _random_state_map(rng, self.EVENT_IDS)
|
|
expected_state = dict(initial)
|
|
expected_changed: set[StateKey] = set()
|
|
expected_cleared = _apply_current_state_deltas(
|
|
expected_state, expected_changed, deltas, timeline_state, ME
|
|
)
|
|
self.assertEqual(
|
|
apply_state_deltas_for_test(initial, rust_deltas, timeline_state, ME),
|
|
(expected_state, expected_changed, expected_cleared),
|
|
context,
|
|
)
|