190 lines
5.3 KiB
Python
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,
|
|
)
|