from __future__ import annotations import subprocess import sys from pathlib import Path import pytest from reconcile_text import diff, reconcile, reconcile_with_history, undiff EXAMPLES_DIR = Path(__file__).resolve().parent.parent.parent / "examples" RESOURCES_DIR = Path(__file__).resolve().parent.parent.parent / "tests" / "resources" FILES = ["pride_and_prejudice.txt", "room_with_a_view.txt", "blns.txt"] class TestReconcile: def test_basic_merge(self) -> None: result = reconcile("Hello", "Hello world", "Hi world") assert result["text"] == "Hi world" def test_three_way_merge(self) -> None: parent = "Merging text is hard!" left = "Merging text is easy!" right = "With reconcile, merging documents is hard!" result = reconcile(parent, left, right) assert result["text"] == "With reconcile, merging documents is easy!" def test_with_cursors(self) -> None: result = reconcile( "Hello", {"text": "Hello world", "cursors": [{"id": 3, "position": 2}]}, { "text": "Hi world", "cursors": [{"id": 4, "position": 0}, {"id": 5, "position": 3}], }, ) assert result["text"] == "Hi world" assert result["cursors"] == [ {"id": 3, "position": 0}, {"id": 4, "position": 0}, {"id": 5, "position": 3}, ] def test_character_tokenizer(self) -> None: result = reconcile("abc", "axc", "abyc", "Character") assert result["text"] == "axyc" def test_line_tokenizer(self) -> None: parent = "line1\nline2\nline3\n" left = "line1\nmodified\nline3\n" right = "line1\nline2\nnew line\n" result = reconcile(parent, left, right, "Line") assert result["text"] == "line1\nmodified\nnew line\n" def test_empty_texts(self) -> None: result = reconcile("", "", "") assert result["text"] == "" assert result["cursors"] == [] def test_invalid_tokenizer(self) -> None: with pytest.raises(ValueError, match="Unknown tokenizer"): reconcile("a", "b", "c", "Invalid") # type: ignore[arg-type] class TestReconcileWithHistory: def test_returns_history(self) -> None: result = reconcile_with_history( "Merging text is hard!", "Merging text is easy!", "With reconcile, merging documents is hard!", ) assert result["text"] == "With reconcile, merging documents is easy!" assert len(result["history"]) > 0 assert all("text" in span and "history" in span for span in result["history"]) def test_history_values(self) -> None: valid_histories = { "Unchanged", "AddedFromLeft", "AddedFromRight", "RemovedFromLeft", "RemovedFromRight", } result = reconcile_with_history("Hello", "Hello world", "Hi") for span in result["history"]: assert span["history"] in valid_histories class TestDiff: def test_basic_diff(self) -> None: result = diff("Hello world", "Hello beautiful world") assert isinstance(result, list) assert all(isinstance(item, (int, str)) for item in result) def test_no_change(self) -> None: result = diff("same text", "same text") # A retain-only diff assert all(isinstance(item, int) and item > 0 for item in result) class TestUndiff: def test_roundtrip(self) -> None: original = "Hello world" changed = "Hello beautiful world" d = diff(original, changed) reconstructed = undiff(original, d) assert reconstructed == changed def test_empty_roundtrip(self) -> None: d = diff("", "") assert undiff("", d) == "" def test_invalid_diff(self) -> None: with pytest.raises(ValueError): undiff("short", [100]) class TestExamples: def test_merge_file_stdout(self, tmp_path: Path) -> None: (tmp_path / "base.txt").write_text("Hello world") (tmp_path / "mine.txt").write_text("Hello beautiful world") (tmp_path / "theirs.txt").write_text("Hi world") result = subprocess.run( [ sys.executable, str(EXAMPLES_DIR / "merge_file.py"), str(tmp_path / "mine.txt"), str(tmp_path / "base.txt"), str(tmp_path / "theirs.txt"), ], capture_output=True, text=True, check=True, ) assert result.stdout == "Hi beautiful world" def test_merge_file_output_file(self, tmp_path: Path) -> None: (tmp_path / "base.txt").write_text("Hello world") (tmp_path / "mine.txt").write_text("Hello beautiful world") (tmp_path / "theirs.txt").write_text("Hi world") output = tmp_path / "output.txt" subprocess.run( [ sys.executable, str(EXAMPLES_DIR / "merge_file.py"), str(tmp_path / "mine.txt"), str(tmp_path / "base.txt"), str(tmp_path / "theirs.txt"), str(output), ], capture_output=True, text=True, check=True, ) assert output.read_text() == "Hi beautiful world" class TestDiffUndiffInverse: """Verify diff/undiff roundtrip across large real-world texts.""" @pytest.mark.parametrize("file1", FILES) @pytest.mark.parametrize("file2", FILES) def test_roundtrip_files(self, file1: str, file2: str) -> None: content1 = (RESOURCES_DIR / file1).read_text()[:50000] content2 = (RESOURCES_DIR / file2).read_text()[:50000] changes = diff(content1, content2) actual = undiff(content1, changes) assert actual == content2