import base64 import json import os from pathlib import Path import subprocess import sys import tempfile import unittest from convert import MAX_BYTES, parse_status SCRIPT = Path(__file__).with_name("from_git.py") HASH = b"a" * 40 class Cases(unittest.TestCase): def test_empty_snapshot(self): self.assertEqual(parse_status(b"")["records"], []) def test_special_paths_are_lossless(self): paths = [b"a b", b"a\tb", b"a\nb", b'quote"slash\\', b"-leading", b" leading", b"invalid-\xff"] rows = parse_status(b"".join(b"? " + path + b"\0" for path in paths))["records"] self.assertEqual([base64.b64decode(row["path"]["bytes_base64"]) for row in rows], paths) self.assertIsNone(rows[-1]["path"]["utf8"]) def test_ordinary_and_submodule_flags(self): raw = b"1 .M S.M. 160000 160000 160000 " + HASH + b" " + HASH + b" sub module\0" row = parse_status(raw)["records"][0] self.assertEqual((row["xy"], row["submodule"], row["path"]["utf8"]), (".M", "S.M.", "sub module")) def test_rename_original_chunk_is_not_a_record(self): raw = b"2 RM N... 100644 100644 100644 " + HASH + b" " + HASH + b" R100 new path\0? old\npath\0? next\0" rows = parse_status(raw)["records"] self.assertEqual(len(rows), 2) self.assertEqual(rows[0]["original_path"]["utf8"], "? old\npath") self.assertEqual(rows[1]["path"]["utf8"], "next") def test_unmerged(self): raw = b"u UU N... 100644 100644 100644 100644 " + b" ".join([HASH] * 3) + b" conflict\0" row = parse_status(raw)["records"][0] self.assertEqual((row["kind"], row["xy"], row["stage3_oid"]), ("unmerged", "UU", HASH.decode())) def test_ignored_and_future_headers(self): data = parse_status(b"# future.value something\0! cache x\0") self.assertEqual(data["ignored_headers"], 1) self.assertEqual(data["records"][0]["kind"], "ignored") def test_truncated_and_unknown_records(self): for raw in [b"? no-nul", b"1 .M short\0", b"x strange\0", b"? \0", b"\0"]: with self.subTest(raw=raw), self.assertRaises(ValueError): parse_status(raw) def test_missing_rename_original(self): raw = b"2 R. N... 100644 100644 100644 " + HASH + b" " + HASH + b" R100 destination\0" with self.assertRaisesRegex(ValueError, "missing_original_path"): parse_status(raw) def test_input_limit(self): with self.assertRaisesRegex(ValueError, "input_too_large"): parse_status(b"? " + b"a" * MAX_BYTES + b"\0") def test_real_git_rename_and_worktree_change(self): with tempfile.TemporaryDirectory() as folder: env = {**os.environ, "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_NOSYSTEM": "1"} def git(*args): return subprocess.run(["git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.invalid", *args], cwd=folder, env=env, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).stdout git("init", "-q") original = "old space.txt" destination = "new space.txt" Path(folder, original).write_text("fixture content\n" * 20) git("add", "--", original) git("commit", "-qm", "fixture") git("mv", "--", original, destination) with Path(folder, destination).open("a") as target: target.write("worktree addition\n") completed = subprocess.run([sys.executable, str(SCRIPT)], cwd=folder, env=env, capture_output=True, check=True) data = json.loads(completed.stdout) row = data["records"][0] self.assertEqual((row["xy"], row["path"]["utf8"], row["original_path"]["utf8"]), ("RM", destination, original)) def test_failed_git_is_not_a_clean_repository(self): with tempfile.TemporaryDirectory() as folder: completed = subprocess.run([sys.executable, str(SCRIPT)], cwd=folder, capture_output=True) self.assertEqual(completed.returncode, 1) self.assertEqual(json.loads(completed.stdout)["error"], "git_status_failed") if __name__ == "__main__": unittest.main()