mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 21:27:14 +01:00
pylint scored the suite at 8.58/10, and 277 of the ~300 findings were missing docstrings -- every one of them in crdt_sync/tests/, none in library code. ruff exempts tests from the docstring rules, so the two linters disagreed and the score sat well below the bar. Rather than silence pylint to match ruff, every test class, test function and stub method now says what it pins. Stub arguments that exist only to match a real signature are underscore-prefixed, which is the documented way to say "deliberately unused" and clears the unused-argument findings too. No behaviour change: 259 tests, still 100% branch coverage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCucBhUkHBBHHbsY7bSdGV
230 lines
6.7 KiB
Python
230 lines
6.7 KiB
Python
"""Tests for the pull/merge/push sync orchestration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import MagicMock
|
|
|
|
from crdt_sync import GitHubSyncClient, Record, sync_log
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
import pytest
|
|
|
|
from crdt_sync import Hlc, Log
|
|
|
|
|
|
def _encode(log: Log) -> str:
|
|
return json.dumps(
|
|
{record_id: record.to_dict() for record_id, record in log.items()}
|
|
)
|
|
|
|
|
|
def _decode(text: str) -> Log:
|
|
return {
|
|
record_id: Record.from_dict(data)
|
|
for record_id, data in json.loads(text).items()
|
|
}
|
|
|
|
|
|
def _mock_client(
|
|
*,
|
|
devices: tuple[str, ...] = (),
|
|
files: dict[str, str] | None = None,
|
|
) -> MagicMock:
|
|
"""Build a mock ``GitHubSyncClient`` covering the methods sync_log calls."""
|
|
client = MagicMock(spec=GitHubSyncClient)
|
|
client.list_directory.return_value = list(devices)
|
|
resolved_files = files or {}
|
|
client.get_file_text.side_effect = resolved_files.get
|
|
return client
|
|
|
|
|
|
class TestSyncLog:
|
|
"""Sync log."""
|
|
|
|
def test_pushes_local_log_when_no_other_devices_have_synced(
|
|
self, make_hlc: Callable[..., Hlc]
|
|
) -> None:
|
|
"""Pushes local log when no other devices have synced."""
|
|
local_log = {"a": Record(id="a", fields={"text": ("hello", make_hlc(100))})}
|
|
client = _mock_client()
|
|
|
|
merged = sync_log(
|
|
client=client,
|
|
device_id="pc",
|
|
path_prefix="devices",
|
|
local_log=local_log,
|
|
encode=_encode,
|
|
decode=_decode,
|
|
)
|
|
|
|
assert merged == local_log
|
|
client.put_file_text.assert_called_once()
|
|
assert client.put_file_text.call_args.args[0] == "devices/pc/log.json"
|
|
|
|
def test_skips_its_own_device_id_when_listing(self) -> None:
|
|
"""Skips its own device id when listing."""
|
|
client = _mock_client(
|
|
devices=("pc", "phone"), files={"devices/phone/log.json": "{}"}
|
|
)
|
|
|
|
sync_log(
|
|
client=client,
|
|
device_id="pc",
|
|
path_prefix="devices",
|
|
local_log={},
|
|
encode=_encode,
|
|
decode=_decode,
|
|
)
|
|
|
|
client.get_file_text.assert_called_once_with("devices/phone/log.json")
|
|
|
|
def test_skips_its_legacy_device_id_as_well_as_its_current_one(self) -> None:
|
|
"""A migrated device must not re-merge its own pre-migration log.
|
|
|
|
Without this the old path looks like a peer, so the device pulls
|
|
back everything it pushed under its former id every single tick.
|
|
"""
|
|
client = _mock_client(
|
|
devices=("pc", "new-uuid", "phone"),
|
|
files={"devices/phone/log.json": "{}"},
|
|
)
|
|
|
|
sync_log(
|
|
client=client,
|
|
device_id="new-uuid",
|
|
path_prefix="devices",
|
|
local_log={},
|
|
encode=_encode,
|
|
decode=_decode,
|
|
legacy_device_id="pc",
|
|
)
|
|
|
|
client.get_file_text.assert_called_once_with("devices/phone/log.json")
|
|
|
|
def test_pulls_the_old_path_when_no_legacy_id_is_declared(self) -> None:
|
|
"""Sanity check the previous test: absent the legacy id, it *is* pulled."""
|
|
client = _mock_client(
|
|
devices=("pc", "new-uuid"), files={"devices/pc/log.json": "{}"}
|
|
)
|
|
|
|
sync_log(
|
|
client=client,
|
|
device_id="new-uuid",
|
|
path_prefix="devices",
|
|
local_log={},
|
|
encode=_encode,
|
|
decode=_decode,
|
|
)
|
|
|
|
client.get_file_text.assert_called_once_with("devices/pc/log.json")
|
|
|
|
def test_skips_a_device_with_no_pushed_file_yet(self) -> None:
|
|
"""Skips a device with no pushed file yet."""
|
|
client = _mock_client(devices=("phone",), files={})
|
|
|
|
merged = sync_log(
|
|
client=client,
|
|
device_id="pc",
|
|
path_prefix="devices",
|
|
local_log={},
|
|
encode=_encode,
|
|
decode=_decode,
|
|
)
|
|
|
|
assert merged == {}
|
|
|
|
def test_skips_a_device_whose_pushed_file_is_corrupt(
|
|
self, caplog: pytest.LogCaptureFixture
|
|
) -> None:
|
|
"""An interrupted/truncated push must not crash every other device's
|
|
merge -- it is treated the same as a device that hasn't pushed yet.
|
|
"""
|
|
client = _mock_client(
|
|
devices=("phone",),
|
|
files={"devices/phone/log.json": "{not valid json"},
|
|
)
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
merged = sync_log(
|
|
client=client,
|
|
device_id="pc",
|
|
path_prefix="devices",
|
|
local_log={},
|
|
encode=_encode,
|
|
decode=_decode,
|
|
)
|
|
|
|
assert merged == {}
|
|
assert "Unparsable log" in caplog.text
|
|
|
|
def test_skips_a_device_whose_pushed_json_has_the_wrong_shape(self) -> None:
|
|
"""Valid JSON that isn't a record map (e.g. from an incompatible
|
|
writer) must be skipped like corrupt JSON, not crash the whole
|
|
sync -- this is what the broad ``(ValueError, KeyError, TypeError)``
|
|
catch in ``_pull_remote_logs`` is for, not just JSON syntax errors.
|
|
"""
|
|
client = _mock_client(
|
|
devices=("phone",),
|
|
files={"devices/phone/log.json": '{"a": 5}'},
|
|
)
|
|
|
|
merged = sync_log(
|
|
client=client,
|
|
device_id="pc",
|
|
path_prefix="devices",
|
|
local_log={},
|
|
encode=_encode,
|
|
decode=_decode,
|
|
)
|
|
|
|
assert merged == {}
|
|
|
|
def test_merges_in_a_remote_devices_entries(
|
|
self, make_hlc: Callable[..., Hlc]
|
|
) -> None:
|
|
"""Merges in a remote devices entries."""
|
|
remote_log = {
|
|
"b": Record(id="b", fields={"text": ("from phone", make_hlc(100))})
|
|
}
|
|
client = _mock_client(
|
|
devices=("phone",),
|
|
files={"devices/phone/log.json": _encode(remote_log)},
|
|
)
|
|
|
|
merged = sync_log(
|
|
client=client,
|
|
device_id="pc",
|
|
path_prefix="devices",
|
|
local_log={},
|
|
encode=_encode,
|
|
decode=_decode,
|
|
)
|
|
|
|
assert merged == remote_log
|
|
|
|
def test_uses_a_custom_filename_and_commit_message(self) -> None:
|
|
"""Uses a custom filename and commit message."""
|
|
client = _mock_client()
|
|
|
|
sync_log(
|
|
client=client,
|
|
device_id="pc",
|
|
path_prefix="devices",
|
|
local_log={},
|
|
encode=_encode,
|
|
decode=_decode,
|
|
filename="notes.json",
|
|
commit_message="custom message",
|
|
)
|
|
|
|
client.put_file_text.assert_called_once_with(
|
|
"devices/pc/notes.json",
|
|
"{}",
|
|
message="custom message",
|
|
)
|