fix(xcf_stack): run last, configurable timeout, tolerate GIMP exit hang
Defer xcf_stack to the final pipeline step via runs_last, expose per-image timeout (default 120s), accept XCF output when gimp-console hangs on quit, and discover .xcf outputs for resume. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -40,6 +41,38 @@ class TestModuleRegistration:
|
||||
def test_get_module_returns_xcf_stack_class(self) -> None:
|
||||
assert get_module("xcf_stack") is XcfStackModule
|
||||
|
||||
def test_xcf_stack_runs_last(self) -> None:
|
||||
from imagepipeline.core.runner import PipelineRunner
|
||||
from imagepipeline.core.step import StepDefinition
|
||||
from imagepipeline.modules.imagemagick_grayscale import ImageMagickGrayscale
|
||||
|
||||
steps = [
|
||||
StepDefinition(
|
||||
step_id="xcf_stack_01",
|
||||
module_name="xcf_stack",
|
||||
module=XcfStackModule,
|
||||
input_refs=["input"],
|
||||
params={},
|
||||
output_dir_name="xcf_stack_01",
|
||||
),
|
||||
StepDefinition(
|
||||
step_id="imagemagick_grayscale_01",
|
||||
module_name="imagemagick_grayscale",
|
||||
module=ImageMagickGrayscale,
|
||||
input_refs=["input"],
|
||||
params={},
|
||||
output_dir_name="imagemagick_grayscale_01",
|
||||
),
|
||||
]
|
||||
runner = PipelineRunner(
|
||||
name="order",
|
||||
input_dir=Path("/tmp/unused"),
|
||||
output_base=Path("/tmp/unused"),
|
||||
steps=steps,
|
||||
)
|
||||
ordered = runner._topological_sort()
|
||||
assert ordered[-1].module_name == "xcf_stack"
|
||||
|
||||
|
||||
class TestExpectedOutputFilenames:
|
||||
def test_returns_xcf_for_jpg_input(self) -> None:
|
||||
@@ -50,6 +83,14 @@ class TestExpectedOutputFilenames:
|
||||
)
|
||||
assert names == ["photo.xcf"]
|
||||
|
||||
def test_default_timeout(self) -> None:
|
||||
params = XcfStackModule.validate_module_params({})
|
||||
assert params["timeout"] == 120.0
|
||||
|
||||
def test_custom_timeout(self) -> None:
|
||||
params = XcfStackModule.validate_module_params({"timeout": 45})
|
||||
assert params["timeout"] == 45.0
|
||||
|
||||
|
||||
def _make_stack_fixture(tmp_path: Path) -> dict[str, Path]:
|
||||
root = tmp_path
|
||||
@@ -185,3 +226,30 @@ class TestStackImagesToXcfIntegration:
|
||||
names = outfile.read_bytes()
|
||||
assert b"bottom" in names
|
||||
assert b"top" in names
|
||||
|
||||
|
||||
class TestGimpTimeoutHandling:
|
||||
def test_accepts_outfile_when_gimp_hangs_on_exit(self, tmp_path: Path) -> None:
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from imagepipeline.utils.gimp import stack_images_to_xcf
|
||||
|
||||
layer = tmp_path / "layer.png"
|
||||
layer.write_bytes(b"png")
|
||||
outfile = tmp_path / "stack.xcf"
|
||||
outfile.write_bytes(b"xcf-data")
|
||||
|
||||
def fake_popen(*_args, **_kwargs):
|
||||
process = MagicMock()
|
||||
process.communicate.side_effect = [
|
||||
subprocess.TimeoutExpired(cmd="gimp", timeout=1),
|
||||
("", ""),
|
||||
]
|
||||
process.kill = MagicMock()
|
||||
return process
|
||||
|
||||
with patch("imagepipeline.utils.gimp.subprocess.Popen", side_effect=fake_popen):
|
||||
with patch("imagepipeline.utils.gimp.require_gimp", return_value="gimp-console"):
|
||||
stack_images_to_xcf([("layer", layer)], outfile, timeout=1.0)
|
||||
|
||||
assert outfile.read_bytes() == b"xcf-data"
|
||||
|
||||
Reference in New Issue
Block a user