项目文件夹

文件
wehub-resource-sync cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

198 行
6.2 KiB
Python

"""Tests for SaveImageToFileInvocation."""
from pathlib import Path
from unittest.mock import MagicMock
import pytest
from PIL import Image
from pydantic import ValidationError
from invokeai.app.invocations.image import SaveImageToFileInvocation
def _make_context(tmp_path: Path, pil_image: Image.Image, gallery_uuid: str = "abc123") -> MagicMock:
context = MagicMock()
context.config.get.return_value.outputs_path = tmp_path
context.images.get_pil.return_value = pil_image
image_dto = MagicMock()
image_dto.image_name = f"{gallery_uuid}.png"
image_dto.width = pil_image.width
image_dto.height = pil_image.height
context.images.save.return_value = image_dto
return context
def _build_node(**overrides) -> SaveImageToFileInvocation:
defaults = {
"id": "test",
"image": {"image_name": "input.png"},
}
defaults.update(overrides)
return SaveImageToFileInvocation(**defaults)
class TestSaveImageToFileInvocation:
def test_saves_to_gallery(self, tmp_path):
img = Image.new("RGB", (8, 8), (255, 0, 0))
ctx = _make_context(tmp_path, img)
node = _build_node()
node.invoke(ctx)
ctx.images.save.assert_called_once()
assert ctx.images.save.call_args.kwargs["image"] is img
def test_default_directory_is_outputs_root(self, tmp_path):
img = Image.new("RGB", (8, 8))
ctx = _make_context(tmp_path, img, gallery_uuid="uuid-1")
node = _build_node()
node.invoke(ctx)
assert (tmp_path / "uuid-1.png").exists()
def test_relative_subdirectory_created(self, tmp_path):
img = Image.new("RGB", (8, 8))
ctx = _make_context(tmp_path, img, gallery_uuid="uuid-2")
node = _build_node(output_directory="my-exports")
node.invoke(ctx)
assert (tmp_path / "my-exports" / "uuid-2.png").exists()
def test_nested_relative_path(self, tmp_path):
img = Image.new("RGB", (8, 8))
ctx = _make_context(tmp_path, img, gallery_uuid="uuid-3")
node = _build_node(output_directory="exports/2026/hero")
node.invoke(ctx)
assert (tmp_path / "exports" / "2026" / "hero" / "uuid-3.png").exists()
def test_prefix_and_suffix_applied(self, tmp_path):
img = Image.new("RGB", (8, 8))
ctx = _make_context(tmp_path, img, gallery_uuid="xyz")
node = _build_node(prefix="hero_", suffix="_final")
node.invoke(ctx)
assert (tmp_path / "hero_xyz_final.png").exists()
def test_prefix_only(self, tmp_path):
img = Image.new("RGB", (8, 8))
ctx = _make_context(tmp_path, img, gallery_uuid="u")
node = _build_node(prefix="p_")
node.invoke(ctx)
assert (tmp_path / "p_u.png").exists()
def test_suffix_only(self, tmp_path):
img = Image.new("RGB", (8, 8))
ctx = _make_context(tmp_path, img, gallery_uuid="u")
node = _build_node(suffix="_s")
node.invoke(ctx)
assert (tmp_path / "u_s.png").exists()
def test_filename_uses_gallery_uuid_not_input_uuid(self, tmp_path):
"""The exported filename must use the UUID from the new gallery entry,
not the UUID of the input image_name."""
img = Image.new("RGB", (8, 8))
ctx = _make_context(tmp_path, img, gallery_uuid="new-uuid")
node = _build_node(image={"image_name": "old-uuid.png"})
node.invoke(ctx)
assert (tmp_path / "new-uuid.png").exists()
assert not (tmp_path / "old-uuid.png").exists()
@pytest.mark.parametrize(
"bad_path",
[
"D:/Pictures/Invoke",
"C:/Windows",
"/etc/passwd",
"/tmp/foo",
],
)
def test_absolute_paths_rejected(self, tmp_path, bad_path):
img = Image.new("RGB", (8, 8))
ctx = _make_context(tmp_path, img)
node = _build_node(output_directory=bad_path)
with pytest.raises(ValueError, match="[Aa]bsolute"):
node.invoke(ctx)
@pytest.mark.parametrize(
"traversal_path",
[
"../outside",
"subdir/../../outside",
"..",
],
)
def test_path_traversal_rejected(self, tmp_path, traversal_path):
img = Image.new("RGB", (8, 8))
ctx = _make_context(tmp_path, img)
node = _build_node(output_directory=traversal_path)
with pytest.raises(ValueError):
node.invoke(ctx)
def test_png_format(self, tmp_path):
img = Image.new("RGBA", (8, 8), (10, 20, 30, 128))
ctx = _make_context(tmp_path, img, gallery_uuid="u")
node = _build_node(file_format="png")
node.invoke(ctx)
path = tmp_path / "u.png"
assert path.exists()
with Image.open(path) as saved:
assert saved.format == "PNG"
assert saved.mode == "RGBA"
def test_jpg_format_converts_rgba_to_rgb(self, tmp_path):
img = Image.new("RGBA", (8, 8), (10, 20, 30, 128))
ctx = _make_context(tmp_path, img, gallery_uuid="u")
node = _build_node(file_format="jpg", quality=80)
node.invoke(ctx)
path = tmp_path / "u.jpg"
assert path.exists()
with Image.open(path) as saved:
assert saved.format == "JPEG"
assert saved.mode == "RGB"
def test_webp_format(self, tmp_path):
img = Image.new("RGB", (8, 8))
ctx = _make_context(tmp_path, img, gallery_uuid="u")
node = _build_node(file_format="webp", quality=75)
node.invoke(ctx)
path = tmp_path / "u.webp"
assert path.exists()
with Image.open(path) as saved:
assert saved.format == "WEBP"
def test_quality_bounds_enforced_by_pydantic(self):
with pytest.raises(ValidationError):
_build_node(quality=0)
with pytest.raises(ValidationError):
_build_node(quality=101)
def test_output_is_pass_through_of_gallery_dto(self, tmp_path):
img = Image.new("RGB", (16, 24))
ctx = _make_context(tmp_path, img, gallery_uuid="uuid-out")
node = _build_node()
result = node.invoke(ctx)
assert result.image.image_name == "uuid-out.png"
assert result.width == 16
assert result.height == 24