124 lines
6.0 KiB
Python
124 lines
6.0 KiB
Python
import unittest
|
|
|
|
from tests.fakes import FakeGitea, FakeJev, FakeModel, config, diff_for, gitea_file
|
|
from tests.test_config_layers import host_data, repo
|
|
from tests.test_flow import run
|
|
from werkrevisor.config import ConfigError, ImagesConfig, build_config
|
|
from werkrevisor.images import check, is_image
|
|
from werkrevisor.rules import ChangedFile
|
|
from werkrevisor.verdict import Verdict
|
|
|
|
DOC = "docs/prs/2026-10-13-PR#7-readme.md"
|
|
SCREENSHOT = "doc/images/dashboard.png"
|
|
PNG = b"\x89PNG\r\n\x1a\n"
|
|
|
|
|
|
def png(size):
|
|
return PNG + b"\0" * (size - len(PNG))
|
|
|
|
|
|
def image_gitea(images):
|
|
gitea = FakeGitea(files=[gitea_file(DOC)] + [gitea_file(path, status="added", additions=0) for path in images],
|
|
diff=diff_for(DOC, added=["Text."], new=True))
|
|
gitea.raw_files.update(images)
|
|
return gitea
|
|
|
|
|
|
def codes(outcome):
|
|
return {r.code: r for r in outcome.result.reasons}
|
|
|
|
|
|
class ImageFlowTest(unittest.TestCase):
|
|
def test_a_small_image_gives_only_a_hint(self):
|
|
outcome, _ = run(image_gitea({SCREENSHOT: png(176871)}), FakeModel(prompt_files=[DOC]))
|
|
self.assertIs(outcome.result.verdict, Verdict.GREEN)
|
|
found = codes(outcome)
|
|
self.assertEqual(found["images"].text,
|
|
f"images checked by size and signature only: {SCREENSHOT} (177 KB)")
|
|
self.assertNotIn("unreviewable_files", found)
|
|
self.assertNotIn("category", found)
|
|
|
|
def test_a_small_image_needs_no_judgment_by_jev(self):
|
|
cfg = config(engine={"mode": "combined", "model_role": "advises"}, gate={"yellow_gate": "block"},
|
|
path_judge={"judge": "jev", "scope": "all"})
|
|
model = FakeModel(prompt_files=[DOC])
|
|
outcome, _ = run(image_gitea({SCREENSHOT: png(176871)}), model, cfg=cfg, jev=FakeJev())
|
|
self.assertIs(outcome.result.verdict, Verdict.GREEN)
|
|
self.assertNotIn("judged_paths_not_judged", codes(outcome))
|
|
self.assertEqual(model.excluded, [(SCREENSHOT,)])
|
|
|
|
def test_an_image_above_max_bytes_is_yellow(self):
|
|
gitea = image_gitea({SCREENSHOT: png(512001)})
|
|
outcome, _ = run(gitea, FakeModel(prompt_files=[DOC]))
|
|
self.assertIs(outcome.result.verdict, Verdict.YELLOW)
|
|
self.assertEqual(codes(outcome)["image_too_large"].text,
|
|
f"images above [images] max_bytes = 512 KB: {SCREENSHOT}; reduce them, e.g. with pngquant")
|
|
|
|
def test_images_above_max_total_bytes_are_yellow(self):
|
|
images = {f"doc/images/{n}.png": png(450000) for n in range(5)}
|
|
outcome, _ = run(image_gitea(images), FakeModel(prompt_files=[DOC]))
|
|
self.assertIs(outcome.result.verdict, Verdict.YELLOW)
|
|
self.assertEqual(codes(outcome)["images_too_large"].text,
|
|
"images of 2250 KB in all exceed [images] max_total_bytes = 2000 KB")
|
|
|
|
def test_another_content_with_an_image_extension_is_yellow(self):
|
|
outcome, _ = run(image_gitea({"doc/logo.png": b"PK\x03\x04archive"}), FakeModel(prompt_files=[DOC]))
|
|
self.assertIs(outcome.result.verdict, Verdict.YELLOW)
|
|
self.assertEqual(codes(outcome)["image_signature"].text,
|
|
"files whose content is no image of their extension: doc/logo.png")
|
|
|
|
def test_a_pr_of_images_only_needs_no_model(self):
|
|
gitea = FakeGitea(files=[gitea_file(SCREENSHOT, status="added", additions=0)], diff="")
|
|
gitea.raw_files[SCREENSHOT] = png(1000)
|
|
model = FakeModel(prompt_files=[])
|
|
outcome, _ = run(gitea, model)
|
|
self.assertIs(outcome.result.verdict, Verdict.GREEN)
|
|
self.assertEqual(model.calls, [])
|
|
self.assertEqual(outcome.result.llm_status, "skipped (images and mechanical changes only)")
|
|
|
|
|
|
class ImageCheckTest(unittest.TestCase):
|
|
def test_signatures(self):
|
|
for path, content, matches in (("a.jpg", b"\xff\xd8\xff\xe0", True), ("a.JPEG", b"\xff\xd8\xff\xe1", True),
|
|
("a.gif", b"GIF89a", True), ("a.ico", b"\0\0\1\0", True),
|
|
("a.webp", b"RIFF\0\0\0\0WEBPVP8 ", True), ("a.webp", b"RIFF\0\0\0\0WAVE", False),
|
|
("a.png", b"\xff\xd8\xff", False)):
|
|
with self.subTest(path=path, content=content):
|
|
reasons = check([ChangedFile(path, "added")], lambda p, limit: content, ImagesConfig())
|
|
self.assertEqual([r.code for r in reasons], ["images" if matches else "image_signature"])
|
|
|
|
def test_reads_at_most_one_byte_above_max_bytes(self):
|
|
limits = []
|
|
check([ChangedFile("a.png", "added")], lambda p, limit: limits.append(limit) or PNG, ImagesConfig())
|
|
self.assertEqual(limits, [512001])
|
|
|
|
def test_a_deleted_image_is_not_read(self):
|
|
def fetch(path, limit):
|
|
raise AssertionError(path)
|
|
self.assertEqual(check([ChangedFile("a.png", "removed")], fetch, ImagesConfig()), [])
|
|
|
|
def test_only_known_formats_within_the_globs_are_images(self):
|
|
self.assertTrue(is_image("doc/a.png", ImagesConfig()))
|
|
self.assertFalse(is_image("doc/a.svg", ImagesConfig()))
|
|
self.assertFalse(is_image("doc/a.bmp", ImagesConfig(globs=("**/*",))))
|
|
self.assertFalse(is_image("doc/a.png", ImagesConfig(globs=("src/**",))))
|
|
|
|
|
|
class ImageConfigTest(unittest.TestCase):
|
|
def test_a_repository_may_lower_the_limits(self):
|
|
cfg = build_config(host_data(), repo("[images]\nmax_bytes = 100000\nmax_total_bytes = 500000\n"))
|
|
self.assertEqual((cfg.images.max_bytes, cfg.images.max_total_bytes), (100000, 500000))
|
|
|
|
def test_a_repository_must_not_raise_the_limits(self):
|
|
for key, value in (("max_bytes", 512001), ("max_total_bytes", 2000001)):
|
|
with self.subTest(key=key), self.assertRaisesRegex(ConfigError, f"may only lower it"):
|
|
build_config(host_data(), repo(f"[images]\n{key} = {value}\n"))
|
|
|
|
def test_limits_must_be_positive(self):
|
|
with self.assertRaisesRegex(ConfigError, r"\[images\] max_bytes must be a positive integer"):
|
|
config(images={"max_bytes": 0})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|