import os import subprocess import sys import tempfile import unittest from pathlib import Path TOOL = Path(__file__).resolve().parents[1] / "debt_split.py" INDEX = "docs/dev/TechnicalDebt.md" A_STUB = "- [libSceA](../../core/libs/prx/libSceA/Export.cpp) - `sceAOne` returns 0" A_FUNCTIONAL = "- `sceATwo` in [libSceA](../../core/libs/prx/libSceA/src/Two.cpp) ignores its flags\n - flag 1 is unknown" B_STUB = "- [libSceB](../../core/libs/prx/libSceB/Export.cpp) - `sceBOne` returns 0" C_STUB = "- [libSceC](../../core/libs/prx/libSceC/Export.cpp) - `sceCOne` returns 0" SHADER = "- [Recompiler](../../core/shader/recompiler/Lower.cpp) rounds once, pinned in [a test](../../core/libs/prx/libSceA/tests/Round.cpp)" SHARED = "- A limitation of every library, see [architecture](ARCHITECTURE.md)" def index(stubs, functional): return ("# Project technical debt\n\n### Build\n\n- A toolchain limitation\n\n### Silent stubs\n\n" "Everything throws, except:\n\n" + "\n".join(stubs) + "\n\n### Functional\n\n" + "\n".join(functional) + "\n") ORIGINAL = index([A_STUB, B_STUB, C_STUB], [A_FUNCTIONAL, SHADER, SHARED]) class DebtSplitTests(unittest.TestCase): def setUp(self): self.directory = tempfile.TemporaryDirectory() self.root = Path(self.directory.name) self.git("init", "-q", "-b", "main") self.write(INDEX, ORIGINAL) self.git("add", ".") self.git("commit", "-qm", "docs: record technical debt") def tearDown(self): self.directory.cleanup() def git(self, *args, check=True): return subprocess.run( ["git", "-c", "user.name=Debt Test", "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", *args], cwd=self.root, capture_output=True, text=True, check=check, env={**os.environ, "GIT_EDITOR": "true"}, ) def write(self, path, text): (self.root / path).parent.mkdir(parents=True, exist_ok=True) (self.root / path).write_text(text, encoding="utf-8") def read(self, path): return (self.root / path).read_text(encoding="utf-8") def tool(self, command): return subprocess.run([sys.executable, str(TOOL), command], cwd=self.root, capture_output=True, text=True, timeout=20) def split(self): result = self.tool("split") self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.git("add", "-A") self.git("commit", "-qm", "docs: split technical debt") def test_split_moves_entries_by_their_first_core_link(self): self.split() self.assertEqual(self.read("docs/dev/debt/libSceA.md"), "# libSceA technical debt\n\n### Silent stubs\n\n" + A_STUB.replace("../../", "../../../") + "\n\n### Functional\n\n" + A_FUNCTIONAL.replace("../../", "../../../") + "\n") self.assertIn(SHADER.replace("../../", "../../../"), self.read("docs/dev/debt/shader.md")) text = self.read(INDEX) for entry in (A_STUB, B_STUB, C_STUB, SHADER): self.assertNotIn(entry.split(" - ")[0], text) for kept in ("- A toolchain limitation", "Everything throws, except:", SHARED, "### Silent stubs", "[debt](debt/)"): self.assertIn(kept, text) def test_split_again_appends_to_existing_files(self): self.split() moved = self.read("docs/dev/debt/libSceA.md") extra = "- [libSceA](../../core/libs/prx/libSceA/Three.cpp) - `sceAThree` returns 0" self.write(INDEX, self.read(INDEX).replace("Everything throws, except:\n", "Everything throws, except:\n\n" + extra)) result = self.tool("split") self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertEqual(result.stdout.split(), ["docs/dev/debt/libSceA.md"]) stub = A_STUB.replace("../../", "../../../") self.assertEqual(self.read("docs/dev/debt/libSceA.md"), moved.replace(stub, stub + "\n" + extra.replace("../../", "../../../"))) self.assertEqual(self.read(INDEX).count("[debt](debt/)"), 1) def rebase(self, stubs, functional): self.git("checkout", "-qb", "change") self.write(INDEX, index(stubs, functional)) self.git("commit", "-qam", "feat: change debt") self.git("checkout", "-q", "main") self.split() self.git("checkout", "-q", "change") rebase = self.git("rebase", "main", check=False) self.assertNotEqual(rebase.returncode, 0, rebase.stdout + rebase.stderr) return self.tool("resolve") def test_resolve_moves_added_edited_and_removed_entries(self): added = "- [libSceA](../../core/libs/prx/libSceA/Export.cpp) - `sceAFour` returns 0" edited = B_STUB + " and logs once" result = self.rebase([A_STUB, added, edited], [A_FUNCTIONAL, SHADER, SHARED]) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) self.assertEqual(sorted(result.stdout.split()), ["docs/dev/debt/libSceA.md", "docs/dev/debt/libSceB.md", "docs/dev/debt/libSceC.md"]) self.assertEqual(self.git("diff", "--name-only", "--diff-filter=U").stdout, "") self.git("rebase", "--continue") a = self.read("docs/dev/debt/libSceA.md") self.assertLess(a.index("`sceAOne`"), a.index("`sceAFour`")) self.assertLess(a.index("`sceAFour`"), a.index("### Functional")) self.assertIn(edited.replace("../../", "../../../"), self.read("docs/dev/debt/libSceB.md")) self.assertFalse((self.root / "docs/dev/debt/libSceC.md").exists()) self.assertEqual(self.read(INDEX), self.git("show", "main:" + INDEX).stdout) def test_resolve_refuses_changes_to_lines_that_stay_in_the_index(self): result = self.rebase([A_STUB, B_STUB, C_STUB], [A_FUNCTIONAL, SHADER, SHARED + " too"]) self.assertNotEqual(result.returncode, 0) self.assertIn("resolve those by hand", result.stderr) self.assertIn("<<<<<<<", self.read(INDEX)) def test_resolve_refuses_an_entry_main_changed_too(self): self.git("checkout", "-qb", "change") self.write(INDEX, index([A_STUB, B_STUB + " here", C_STUB], [A_FUNCTIONAL, SHADER, SHARED])) self.git("commit", "-qam", "feat: change b") self.git("checkout", "-q", "main") self.split() path = "docs/dev/debt/libSceB.md" self.write(path, self.read(path).replace("returns 0", "returns 1")) self.git("commit", "-qam", "docs: change b") self.git("checkout", "-q", "change") self.assertNotEqual(self.git("rebase", "main", check=False).returncode, 0) result = self.tool("resolve") self.assertNotEqual(result.returncode, 0) self.assertIn("entry not found", result.stderr) def test_resolve_needs_a_conflict(self): result = self.tool("resolve") self.assertNotEqual(result.returncode, 0) self.assertIn("not in a three-way conflict", result.stderr) if __name__ == "__main__": unittest.main()