swayfx-mosaic/tests/test_export_patch.py

175 lines
7.6 KiB
Python

"""Offline regression checks for the cumulative package patch exporter."""
import hashlib
import os
from pathlib import Path
import shlex
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
PATCH = Path("patches/0001-mosaic-layout-viewport.patch")
BASE = "a57e3f2c4d88f258c2f038343a398c6d5be1fb59"
def run(*args, cwd, env=None, check=True):
return subprocess.run(
args, cwd=cwd, env=env, check=check, text=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
)
def digest(data):
return hashlib.sha256(data).hexdigest()
class ExportPatchTest(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="mosaic-export-test-")
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.clone = self.root / "clone"
# The clone retains the imported base but never changes the real repo.
run("git", "clone", "-q", "--no-hardlinks", str(ROOT), str(self.clone), cwd=self.root)
shutil.copy2(ROOT / "tooling/export-patch.sh", self.clone / "tooling/export-patch.sh")
shutil.copy2(ROOT / PATCH, self.clone / PATCH)
def export(self, *, env=None, check=True):
return run("sh", "tooling/export-patch.sh", cwd=self.clone, env=env, check=check)
def test_complete_idempotent_and_index_untouched(self):
original = (self.clone / PATCH).read_bytes()
self.export()
first = (self.clone / PATCH).read_bytes()
self.assertTrue(first)
self.assertEqual(first, original)
self.export()
self.assertEqual((self.clone / PATCH).read_bytes(), first)
# One staged source edit plus one later working-tree edit must both
# appear, while the real staged index remains byte-identical.
source = self.clone / "sway/tree/mosaic.c"
with source.open("a") as file:
file.write("\n/* export staged marker */\n")
run("git", "add", "sway/tree/mosaic.c", cwd=self.clone)
with source.open("a") as file:
file.write("/* export working marker */\n")
index = self.clone / ".git/index"
index_before = index.read_bytes()
objects_before = sorted(str(path.relative_to(self.clone / ".git/objects"))
for path in (self.clone / ".git/objects").rglob("*")
if path.is_file())
staged_before = run("git", "diff", "--cached", "--binary", cwd=self.clone).stdout
self.export()
changed = (self.clone / PATCH).read_bytes()
self.assertIn(b"export staged marker", changed)
self.assertIn(b"export working marker", changed)
self.assertEqual(index.read_bytes(), index_before)
self.assertEqual(sorted(str(path.relative_to(self.clone / ".git/objects"))
for path in (self.clone / ".git/objects").rglob("*")
if path.is_file()), objects_before)
self.assertEqual(run("git", "diff", "--cached", "--binary", cwd=self.clone).stdout,
staged_before)
self.export()
self.assertEqual((self.clone / PATCH).read_bytes(), changed)
def test_untracked_source_and_failure_preserve_patch(self):
source = self.clone / "include/sway/tree/export-probe.h"
source.write_text("/* untracked source marker */\n")
self.export()
exported = (self.clone / PATCH).read_bytes()
self.assertIn(b"untracked source marker", exported)
# Ignored files cannot be exported by Git. Source equivalence must
# reject this late validation failure before replacing the artifact.
ignored = self.clone / "include/sway/tree/export-probe.o"
ignored.write_text("ignored source marker\n")
failed = self.export(check=False)
self.assertNotEqual(failed.returncode, 0)
self.assertEqual(digest((self.clone / PATCH).read_bytes()), digest(exported))
# Also cover an early temporary-allocation failure.
env = os.environ.copy()
env["TMPDIR"] = str(self.root / "missing")
failed = self.export(env=env, check=False)
self.assertNotEqual(failed.returncode, 0)
self.assertEqual(digest((self.clone / PATCH).read_bytes()), digest(exported))
def test_source_deletion_is_exported(self):
source = self.clone / "include/sway/tree/node.h"
source.unlink()
self.export()
patch = (self.clone / PATCH).read_bytes()
self.assertIn(b"deleted file mode", patch)
self.assertIn(b"include/sway/tree/node.h", patch)
def test_unmerged_source_index_preserves_patch(self):
original = (self.clone / PATCH).read_bytes()
path = "include/sway/tree/container.h"
blob = run("git", "rev-parse", f"HEAD:{path}", cwd=self.clone).stdout.strip()
run("git", "update-index", "--force-remove", path, cwd=self.clone)
subprocess.run(
["git", "update-index", "--index-info"], cwd=self.clone,
input=f"100644 {blob} 1\t{path}\n100644 {blob} 2\t{path}\n",
text=True, check=True, capture_output=True,
)
index = self.clone / ".git/index"
index_before = index.read_bytes()
failed = self.export(check=False)
self.assertNotEqual(failed.returncode, 0)
self.assertIn("unresolved source conflicts", failed.stderr)
self.assertEqual(index.read_bytes(), index_before)
self.assertEqual((self.clone / PATCH).read_bytes(), original)
def test_source_mode_change_is_exported(self):
source = self.clone / "sway/tree/mosaic.c"
source.chmod(0o755)
self.export()
patch = (self.clone / PATCH).read_bytes()
self.assertIn(b"new file mode 100755", patch)
self.assertEqual(source.stat().st_mode & 0o777, 0o755)
def test_interruption_preserves_patch_and_cleans_temporary_files(self):
original = (self.clone / PATCH).read_bytes()
tool_dir = self.root / "tools"
tool_dir.mkdir()
wrapper = tool_dir / "git"
wrapper.write_text(
"#!/bin/sh\n"
"if test \"$1\" = archive; then kill -TERM \"$PPID\"; exit 1; fi\n"
f"exec {shlex.quote(shutil.which('git'))} \"$@\"\n"
)
wrapper.chmod(0o755)
scratch = self.root / "scratch"
scratch.mkdir()
env = os.environ.copy()
env["PATH"] = f"{tool_dir}:{env['PATH']}"
env["TMPDIR"] = str(scratch)
failed = self.export(env=env, check=False)
self.assertNotEqual(failed.returncode, 0)
self.assertEqual((self.clone / PATCH).read_bytes(), original)
self.assertEqual(list((self.clone / "patches").glob(".mosaic-patch.*")), [])
self.assertEqual(list(scratch.iterdir()), [])
def test_forward_apply_and_source_equivalence(self):
self.export()
pristine = self.root / "pristine"
pristine.mkdir()
archive = self.root / "base.tar"
with archive.open("wb") as file:
subprocess.run(["git", "archive", BASE], cwd=self.clone,
check=True, stdout=file)
run("tar", "-xf", str(archive), "-C", str(pristine), cwd=self.root)
run("git", "apply", "--check", str(self.clone / PATCH), cwd=pristine)
run("git", "apply", str(self.clone / PATCH), cwd=pristine)
for name in ("include", "sway", "meson.build", "meson_options.txt",
"tests/mosaic-model.c"):
run("git", "diff", "--no-index", "--exit-code", "--",
str(pristine / name), str(self.clone / name), cwd=self.root)
if __name__ == "__main__":
unittest.main()