A long time ago, in a galaxy far far away...

This commit is contained in:
Frank Schwenk
2026-06-21 10:26:10 +02:00
parent e7cdb8dd6f
commit 980c7f3b8b
15 changed files with 645 additions and 2 deletions
+118
View File
@@ -0,0 +1,118 @@
from __future__ import annotations
import shutil
from pathlib import Path
import pytest
from imagepipeline.core.pipeline import Pipeline
from imagepipeline.core.resume import materialize_external_outputs, step_outputs_complete
from imagepipeline.core.step import StepDefinition
from imagepipeline.modules.imagemagick_grayscale import ImageMagickGrayscale
from imagepipeline.modules.registry import get_module
from imagepipeline.utils.gmic import split_gmic_command
from tests.conftest import make_png
class TestGmicCommandSplit:
def test_splits_command_and_arguments(self) -> None:
assert split_gmic_command("-gcd_stereo_img 0,0,2.028,1,1.714,3.06,4,1,0") == [
"-gcd_stereo_img",
"0,0,2.028,1,1.714,3.06,4,1,0",
]
def test_preserves_quoted_empty_argument(self) -> None:
parts = split_gmic_command('-fx_custom_gradient 0,0,0,"",1,0')
assert parts == ["-fx_custom_gradient", "0,0,0,,1,0"]
class TestPipelineResume:
@pytest.mark.skipif(not shutil.which("magick"), reason="ImageMagick not installed")
def test_continue_skips_completed_steps(self, input_dir: Path, output_base: Path, capsys) -> None:
with Pipeline(
name="resume_test",
input_dir=input_dir,
output_base=output_base,
verbose=True,
) as p:
first = p.step("imagemagick_grayscale", inputs="input")
p.step("imagemagick_grayscale", inputs=first)
root = p.run()
capsys.readouterr()
with Pipeline(
name="resume_test",
input_dir=input_dir,
output_base=output_base,
verbose=True,
continue_from=root,
) as p:
step_a = p.step("imagemagick_grayscale", inputs="input")
p.step("imagemagick_grayscale", inputs=step_a)
resumed_root = p.run()
assert resumed_root == root
output = capsys.readouterr().out
assert "Skipped step imagemagick_grayscale_01" in output
assert "Skipped step imagemagick_grayscale_02" in output
@pytest.mark.skipif(not shutil.which("magick"), reason="ImageMagick not installed")
def test_existing_outputs_reuse_external_folder(
self, input_dir: Path, output_base: Path, tmp_path: Path, capsys
) -> None:
external = tmp_path / "external_rembg"
external.mkdir()
for src in input_dir.iterdir():
if src.is_file():
make_png(external / src.name, width=4, height=4, rgb=(10, 20, 30))
with Pipeline(
name="external_test",
input_dir=input_dir,
output_base=output_base,
verbose=True,
existing_outputs={"imagemagick_grayscale_01": external},
) as p:
reused = p.step("imagemagick_grayscale", inputs="input")
p.step("imagemagick_grayscale", inputs=reused)
root = p.run()
output = capsys.readouterr().out
assert "Reused external output for imagemagick_grayscale_01" in output
assert (root / "imagemagick_grayscale_01" / "photo_a.png").exists()
class TestMaterializeExternal:
def test_links_files_by_stem(self, tmp_path: Path) -> None:
external = tmp_path / "external"
external.mkdir()
make_png(external / "photo_a.png")
output_dir = tmp_path / "out"
step = StepDefinition(
step_id="imagemagick_grayscale_01",
module_name="imagemagick_grayscale",
module=ImageMagickGrayscale,
input_refs=["input"],
params={},
output_dir_name="imagemagick_grayscale_01",
)
input_paths = [tmp_path / "photo_a.png"]
paths = materialize_external_outputs(
external,
output_dir,
step,
matched_groups=[[path] for path in input_paths],
input_paths=input_paths,
params={},
)
assert len(paths) == 1
assert paths[0].name == "photo_a.png"
assert paths[0].is_symlink()
def test_step_outputs_complete(self, tmp_path: Path) -> None:
output_dir = tmp_path / "done"
output_dir.mkdir()
make_png(output_dir / "photo.png")
assert step_outputs_complete([output_dir / "photo.png"])
assert not step_outputs_complete([output_dir / "missing.png"])
+89
View File
@@ -6,6 +6,10 @@ from pathlib import Path
import pytest
from imagepipeline.core.params import validate_params
from imagepipeline.modules.color_to_alpha import (
ColorToAlphaModule,
build_color_to_alpha_args,
)
from imagepipeline.modules.composite import CompositeModule
from imagepipeline.modules.crop_square import CropSquareModule
from imagepipeline.modules.darktable_style import DarktableStyleModule
@@ -14,6 +18,10 @@ from imagepipeline.modules.imagemagick_fill import (
build_fill_arguments,
)
from imagepipeline.modules.imagemagick_grayscale import ImageMagickGrayscale
from imagepipeline.modules.imagemagick_resize import (
ImageMagickResizeModule,
build_resize_arguments,
)
from imagepipeline.modules.gmic_grayscale import GmicGrayscale
from imagepipeline.modules.registry import get_module, list_modules
from imagepipeline.modules.rembg import RembgModule
@@ -31,6 +39,8 @@ class TestModuleRegistration:
"darktable_style",
"imagemagick_grayscale",
"imagemagick_fill",
"color_to_alpha",
"imagemagick_resize",
"crop_square",
):
assert name in names
@@ -42,6 +52,85 @@ class TestModuleRegistration:
assert get_module("darktable_style") is DarktableStyleModule
assert get_module("crop_square") is CropSquareModule
assert get_module("imagemagick_fill") is ImageMagickFillModule
assert get_module("color_to_alpha") is ColorToAlphaModule
class TestImageMagickResize:
def test_build_resize_arguments(self) -> None:
assert build_resize_arguments(max_edge=2000) == [
"-auto-orient",
"-resize",
"2000x2000>",
]
def test_rejects_non_positive_max_edge(self) -> None:
with pytest.raises(ValueError, match="positive"):
build_resize_arguments(max_edge=0)
def test_default_max_edge(self) -> None:
params = ImageMagickResizeModule.validate_module_params({})
assert params["max_edge"] == 2000
class TestColorToAlpha:
def test_build_args_exact(self) -> None:
assert build_color_to_alpha_args(color="#00ff00", fuzz=0.0) == [
"-alpha",
"on",
"-transparent",
"#00ff00",
]
def test_build_args_with_fuzz(self) -> None:
assert build_color_to_alpha_args(color="ffffff", fuzz=2.5) == [
"-alpha",
"on",
"-fuzz",
"2.5%",
"-transparent",
"#ffffff",
]
def test_requires_color(self) -> None:
with pytest.raises(ValueError, match="required"):
ColorToAlphaModule.validate_module_params({})
@pytest.mark.skipif(not has_magick, reason="ImageMagick not installed")
def test_makes_matching_color_transparent(self, tmp_path: Path) -> None:
from imagepipeline.core.context import ModuleContext
from imagepipeline.utils.subprocess import run_command
src = tmp_path / "green.png"
output_dir = tmp_path / "out"
output_dir.mkdir()
magick = shutil.which("magick") or shutil.which("convert")
run_command([magick, "-size", "8x8", "xc:#00ff00", str(src)])
ctx = ModuleContext(
input_paths=[src],
matched_groups=[],
output_dir=output_dir,
params=ColorToAlphaModule.validate_module_params({"color": "#00ff00"}),
pipeline_output_root=tmp_path,
step_id="color_to_alpha_01",
logger=None,
)
ColorToAlphaModule().run(ctx)
dst = output_dir / "green.png"
assert dst.is_file()
result = run_command(
[
magick,
str(dst),
"-alpha",
"extract",
"-format",
"%[fx:mean]",
"info:",
]
)
assert float(result.stdout.strip()) == 0.0
class TestImageMagickFill: