Files
mdpolish/tests/test_artifact_replay.py
T

190 lines
5.3 KiB
Python

from __future__ import annotations
from collections.abc import Callable
from dataclasses import replace
import pytest
from mdpolish._artifact_replay import (
ReplayChange,
ReplayComponent,
ReplayError,
replay_change_chain,
)
from mdpolish.models import markdown_sha256
def change(
*,
component_position: int,
before_text: str,
after_text: str,
start: int,
end: int,
before_sha256: str,
after_sha256: str,
proposal_index: int = 0,
edit_index: int = 0,
) -> ReplayChange:
return ReplayChange(
component_id=f"test.{component_position}",
component_version="1.0.0",
component_position=component_position,
proposal_component_position=component_position,
proposal_snapshot_sha256=before_sha256,
proposal_index=proposal_index,
edit_index=edit_index,
start=start,
end=end,
before=before_text,
after=after_text,
before_sha256=before_sha256,
after_sha256=after_sha256,
)
def test_replay_builds_zero_change_stages_and_utf16_editor_ranges() -> None:
original = "\ufeff😀 old\r\nCafe\u0301"
final = "\ufeff😀 new\r\nCafe\u0301"
input_hash = markdown_sha256(original)
final_hash = markdown_sha256(final)
recorded = change(
component_position=0,
before_text="old",
after_text="new",
start=3,
end=6,
before_sha256=input_hash,
after_sha256=final_hash,
)
replayed = replay_change_chain(
input_markdown=original,
input_sha256=input_hash,
components=(ReplayComponent("test.0", "1.0.0"), ReplayComponent("test.1", "1.0.0")),
changes=(recorded,),
current_sha256=final_hash,
current_markdown=final,
include_zero_change_stages=True,
)
assert replayed.current_markdown == final
assert len(replayed.stages) == 2
assert replayed.stages[1].before_markdown == final
assert replayed.stages[1].after_markdown == final
assert replayed.stages[1].changes == ()
assert (replayed.changes[0].line, replayed.changes[0].column) == (1, 4)
assert (replayed.changes[0].editor_start, replayed.changes[0].editor_end) == (4, 7)
def test_replay_uses_full_descending_application_key_not_record_order() -> None:
original = "abcd"
final = "aXXcYY"
input_hash = markdown_sha256(original)
final_hash = markdown_sha256(final)
right = change(
component_position=0,
before_text="d",
after_text="YY",
start=3,
end=4,
before_sha256=input_hash,
after_sha256=final_hash,
proposal_index=1,
)
left = change(
component_position=0,
before_text="b",
after_text="XX",
start=1,
end=2,
before_sha256=input_hash,
after_sha256=final_hash,
)
replayed = replay_change_chain(
input_markdown=original,
input_sha256=input_hash,
components=(ReplayComponent("test.0", "1.0.0"),),
changes=(right, left),
current_sha256=final_hash,
current_markdown=final,
include_zero_change_stages=True,
)
assert replayed.current_markdown == final
@pytest.mark.parametrize(
("mutate", "message"),
[
(lambda item: replace(item, component_position=2), "component position"),
(lambda item: replace(item, proposal_component_position=1), "proposal reference"),
(lambda item: replace(item, before="bad"), "recorded snapshot"),
(lambda item: replace(item, after_sha256="0" * 64), "batch hash"),
],
)
def test_replay_rejects_untrusted_change_chains(
mutate: Callable[[ReplayChange], ReplayChange], message: str
) -> None:
original = "old"
final = "new"
input_hash = markdown_sha256(original)
final_hash = markdown_sha256(final)
valid = change(
component_position=0,
before_text="old",
after_text="new",
start=0,
end=3,
before_sha256=input_hash,
after_sha256=final_hash,
)
tampered = mutate(valid)
with pytest.raises(ReplayError, match=message):
replay_change_chain(
input_markdown=original,
input_sha256=input_hash,
components=(ReplayComponent("test.0", "1.0.0"),),
changes=(tampered,),
current_sha256=final_hash,
current_markdown=final,
include_zero_change_stages=True,
)
def test_replay_rejects_conflicting_ranges() -> None:
original = "abc"
input_hash = markdown_sha256(original)
first = change(
component_position=0,
before_text="ab",
after_text="x",
start=0,
end=2,
before_sha256=input_hash,
after_sha256="0" * 64,
)
second = change(
component_position=0,
before_text="bc",
after_text="y",
start=1,
end=3,
before_sha256=input_hash,
after_sha256="0" * 64,
proposal_index=1,
)
with pytest.raises(ReplayError, match="conflicting"):
replay_change_chain(
input_markdown=original,
input_sha256=input_hash,
components=(ReplayComponent("test.0", "1.0.0"),),
changes=(first, second),
current_sha256=input_hash,
current_markdown=None,
include_zero_change_stages=False,
)