- gitleaks (via docker cp, dockerignore-agnostic) and hadolint scan every push/PR - new ruff lint stage (ruff.toml pins known-first-party for host/container consistency; F841/B007 in webui.py left un-fixed via per-file-ignores — app-logic changes, flagged for review rather than auto-fixed) - pytest --cov-fail-under=90 gate on the test stage (already had pytest-cov, never wired into CI until now) - scheduled Trivy critical failures now attempt an apk upgrade rebuild and open a PR if it clears the finding, instead of just failing red - ruff --fix/--format applied to existing code to start the gate clean
138 lines
4.9 KiB
Python
138 lines
4.9 KiB
Python
from PIL import Image
|
|
|
|
from src.py.builder import image_processor as ip
|
|
|
|
|
|
def test_convert_and_resize_image_missing_input_logs_and_skips(tmp_path):
|
|
output_path = tmp_path / "out.webp"
|
|
ip.convert_and_resize_image(tmp_path / "missing.jpg", output_path)
|
|
assert not output_path.exists()
|
|
|
|
|
|
def test_convert_and_resize_image_resizes_down(tmp_path, make_image):
|
|
input_path = make_image(tmp_path / "in.jpg", size=(2000, 1000))
|
|
output_path = tmp_path / "out.webp"
|
|
|
|
ip.convert_and_resize_image(input_path, output_path, resize=True, max_width=1000)
|
|
|
|
actual_output = output_path if output_path.exists() else output_path.with_suffix(".jpg")
|
|
assert actual_output.exists()
|
|
with Image.open(actual_output) as img:
|
|
assert img.width == 1000
|
|
assert img.height == 500
|
|
|
|
|
|
def test_convert_and_resize_image_no_resize_keeps_dimensions(tmp_path, make_image):
|
|
input_path = make_image(tmp_path / "in.jpg", size=(500, 300))
|
|
output_path = tmp_path / "out.webp"
|
|
|
|
ip.convert_and_resize_image(input_path, output_path, resize=False)
|
|
|
|
actual_output = output_path if output_path.exists() else output_path.with_suffix(".jpg")
|
|
with Image.open(actual_output) as img:
|
|
assert img.size == (500, 300)
|
|
|
|
|
|
def test_convert_and_resize_image_preserves_icc_profile(tmp_path):
|
|
input_path = tmp_path / "in.jpg"
|
|
icc_bytes = b"fake-icc-profile-bytes"
|
|
Image.new("RGB", (100, 80), (10, 20, 30)).save(input_path, "JPEG", icc_profile=icc_bytes)
|
|
output_path = tmp_path / "out.webp"
|
|
|
|
ip.convert_and_resize_image(input_path, output_path, resize=False)
|
|
|
|
actual_output = output_path if output_path.exists() else output_path.with_suffix(".jpg")
|
|
with Image.open(actual_output) as img:
|
|
assert img.info.get("icc_profile") == icc_bytes
|
|
|
|
|
|
def test_process_images_updates_src_to_processed_extension(tmp_path, make_image):
|
|
img_dir = tmp_path / "photos"
|
|
build_dir = tmp_path / "output"
|
|
make_image(img_dir / "gallery" / "a.jpg", size=(200, 200))
|
|
images = [{"src": "gallery/a.jpg", "tags": []}]
|
|
|
|
ip.process_images(images, resize_images=True, img_dir=img_dir, build_dir=build_dir)
|
|
|
|
assert images[0]["src"] in ("gallery/a.webp", "gallery/a.jpg")
|
|
produced = build_dir / "img" / images[0]["src"]
|
|
assert produced.exists()
|
|
|
|
|
|
def test_copy_original_images_copies_existing_and_skips_missing(tmp_path, make_image, caplog):
|
|
img_dir = tmp_path / "photos"
|
|
build_dir = tmp_path / "output"
|
|
make_image(img_dir / "gallery" / "present.jpg")
|
|
images = [{"src": "gallery/present.jpg"}, {"src": "gallery/absent.jpg"}]
|
|
|
|
ip.copy_original_images(images, img_dir, build_dir)
|
|
|
|
assert (build_dir / "img" / "gallery" / "present.jpg").exists()
|
|
assert not (build_dir / "img" / "gallery" / "absent.jpg").exists()
|
|
|
|
|
|
def test_get_favicon_path_missing_config_returns_none(tmp_path):
|
|
assert ip.get_favicon_path({}, tmp_path) is None
|
|
|
|
|
|
def test_get_favicon_path_relative_and_absolute(tmp_path, make_image):
|
|
theme_dir = tmp_path / "theme"
|
|
icon_path = make_image(theme_dir / "favicon.png", fmt="PNG")
|
|
|
|
result = ip.get_favicon_path({"favicon": {"path": "favicon.png"}}, theme_dir)
|
|
assert result == icon_path
|
|
|
|
absolute_result = ip.get_favicon_path({"favicon": {"path": str(icon_path)}}, theme_dir)
|
|
assert absolute_result == icon_path
|
|
|
|
|
|
def test_get_favicon_path_file_not_found(tmp_path):
|
|
result = ip.get_favicon_path({"favicon": {"path": "missing.png"}}, tmp_path)
|
|
assert result is None
|
|
|
|
|
|
def test_generate_favicons_from_logo_creates_all_sizes(tmp_path, make_image):
|
|
theme_dir = tmp_path / "theme"
|
|
make_image(theme_dir / "favicon.png", size=(256, 256), fmt="PNG")
|
|
output_dir = tmp_path / "output" / "favicon"
|
|
|
|
ip.generate_favicons_from_logo({"favicon": {"path": "favicon.png"}}, theme_dir, output_dir)
|
|
|
|
expected = [
|
|
"favicon-32.png",
|
|
"favicon-96.png",
|
|
"favicon-128.png",
|
|
"favicon-192.png",
|
|
"favicon-196.png",
|
|
"favicon-152.png",
|
|
"favicon-180.png",
|
|
]
|
|
for name in expected:
|
|
assert (output_dir / name).exists()
|
|
with Image.open(output_dir / "favicon-32.png") as img:
|
|
assert img.size == (32, 32)
|
|
|
|
|
|
def test_generate_favicons_from_logo_no_favicon_skips(tmp_path):
|
|
output_dir = tmp_path / "output" / "favicon"
|
|
ip.generate_favicons_from_logo({}, tmp_path, output_dir)
|
|
assert not output_dir.exists()
|
|
|
|
|
|
def test_generate_favicon_ico_creates_file(tmp_path, make_image):
|
|
theme_dir = tmp_path / "theme"
|
|
make_image(theme_dir / "favicon.png", size=(64, 64), fmt="PNG")
|
|
output_path = tmp_path / "output" / "favicon.ico"
|
|
|
|
ip.generate_favicon_ico({"favicon": {"path": "favicon.png"}}, theme_dir, output_path)
|
|
|
|
assert output_path.exists()
|
|
with Image.open(output_path) as img:
|
|
assert img.format == "ICO"
|
|
|
|
|
|
def test_generate_favicon_ico_no_favicon_skips(tmp_path):
|
|
output_path = tmp_path / "output" / "favicon.ico"
|
|
ip.generate_favicon_ico({}, tmp_path, output_path)
|
|
assert not output_path.exists()
|