Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions scripts/visual_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,19 @@ def load_json(path: Path, label: str) -> dict[str, Any]:
return value


def comparison_commit(root: Path, reference: str) -> str:
result = subprocess.run(
["git", "rev-parse", "--verify", "--end-of-options", f"{reference}^{{commit}}"],
cwd=root, check=False, capture_output=True, text=True,
)
commit = result.stdout.strip()
if result.returncode or re.fullmatch(r"(?:[0-9a-f]{40}|[0-9a-f]{64})", commit) is None:
raise VisualEvidenceError("The comparison base must resolve to one Git commit")
return commit


def changed_paths(root: Path, base_ref: str) -> list[str]:
commit = comparison_commit(root, base_ref)
result = subprocess.run(
[
"git",
Expand All @@ -40,7 +52,7 @@ def changed_paths(root: Path, base_ref: str) -> list[str]:
"--no-renames",
"--diff-filter=ACDMRTUXB",
"-z",
f"{base_ref}...HEAD",
f"{commit}...HEAD",
],
cwd=root,
check=True,
Expand All @@ -55,8 +67,9 @@ def file_content(root: Path, path: str, base_ref: str | None = None) -> str:
if candidate.is_file():
contents.append(candidate.read_text(encoding="utf-8", errors="replace"))
if base_ref:
commit = comparison_commit(root, base_ref)
result = subprocess.run(
["git", "show", f"{base_ref}:{path}"],
["git", "show", f"{commit}:{path}"],
cwd=root,
check=False,
capture_output=True,
Expand Down
51 changes: 51 additions & 0 deletions tests/test_visual_evidence.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
VisualEvidenceError,
changed_paths,
classify_changes,
file_content,
load_json,
validate_manifest,
validate_report,
Expand All @@ -25,6 +26,56 @@
SCHEMA_PATH = ROOT / "visual-evidence" / "schema.json"


class VisualEvidenceGitBoundaryTest(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name) / "repository"
self.output = Path(temporary.name) / "outside"
self.root.mkdir()
self.output.mkdir()
self.git("init", "-q")
(self.root / "source.css").write_text("old selector\n")
self.git("add", "source.css")
self.git("commit", "-qm", "baseline")
self.base = self.git("rev-parse", "HEAD").stdout.strip()
self.git("branch", "comparison-base")
(self.root / "source.css").unlink()
self.git("add", "source.css")
self.git("commit", "-qm", "remove source")

def git(self, *arguments):
return subprocess.run(
["git", "-c", "user.name=Visual Evidence Test", "-c",
"user.email=visual-evidence@example.invalid", "-c", "commit.gpgsign=false", *arguments],
cwd=self.root, check=True, capture_output=True, text=True,
)

def test_sha_branch_and_relative_commit_keep_diff_and_deleted_content(self):
for reference in (self.base, "comparison-base", "HEAD~1"):
with self.subTest(reference=reference):
self.assertEqual(["source.css"], changed_paths(self.root, reference))
self.assertEqual("old selector\n", file_content(self.root, "source.css", reference))

def test_option_shaped_refs_refuse_without_writing_outside_repository(self):
reference = "--output=" + str(self.output / "injected")
calls = ((changed_paths, (self.root, reference)),
(file_content, (self.root, "source.css", reference)))
for function, arguments in calls:
with self.subTest(function=function.__name__):
with self.assertRaises(VisualEvidenceError):
function(*arguments)
self.assertEqual([], list(self.output.iterdir()))

def test_missing_and_noncommit_refs_refuse(self):
for reference in ("missing-reference", self.base + ":source.css"):
for function, arguments in ((changed_paths, (self.root, reference)),
(file_content, (self.root, "source.css", reference))):
with self.subTest(reference=reference, function=function.__name__):
with self.assertRaises(VisualEvidenceError):
function(*arguments)


class VisualEvidencePolicyTest(unittest.TestCase):
def setUp(self) -> None:
self.policy = load_json(POLICY_PATH, "visual evidence policy")
Expand Down
Loading