2026-06-16 09:38:06 +08:00
|
|
|
|
"""Tests for template service, file scanning, and batch pair generation.
|
|
|
|
|
|
No GUI dependency — runs in pure Python 3.7.
|
|
|
|
|
|
"""
|
|
|
|
|
|
import json
|
|
|
|
|
|
import shutil
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import tempfile
|
|
|
|
|
|
import unittest
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
|
|
|
|
|
|
|
|
import services.file_service as _file_service
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _patch_config_dir(tmpdir):
|
|
|
|
|
|
"""Return a context-manager that redirects get_config_path to tmpdir."""
|
|
|
|
|
|
class _Ctx:
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
|
|
self._orig = _file_service.get_config_path
|
|
|
|
|
|
_file_service.get_config_path = lambda name: Path(tmpdir) / name
|
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
|
|
def __exit__(self, *_):
|
|
|
|
|
|
_file_service.get_config_path = self._orig
|
|
|
|
|
|
|
|
|
|
|
|
return _Ctx()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Template service
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestBuiltinTemplates(unittest.TestCase):
|
|
|
|
|
|
def test_builtins_are_present(self):
|
|
|
|
|
|
from services.template_service import get_builtin_templates
|
|
|
|
|
|
builtins = get_builtin_templates()
|
|
|
|
|
|
self.assertGreater(len(builtins), 0)
|
|
|
|
|
|
|
|
|
|
|
|
def test_builtins_marked_as_builtin(self):
|
|
|
|
|
|
from services.template_service import get_builtin_templates
|
|
|
|
|
|
for t in get_builtin_templates():
|
|
|
|
|
|
self.assertEqual(t.type, "builtin")
|
|
|
|
|
|
|
|
|
|
|
|
def test_builtins_have_valid_ratios(self):
|
|
|
|
|
|
from services.template_service import get_builtin_templates
|
|
|
|
|
|
for t in get_builtin_templates():
|
|
|
|
|
|
self.assertGreater(t.width_ratio, 0)
|
|
|
|
|
|
self.assertGreater(t.height_ratio, 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCustomTemplates(unittest.TestCase):
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
self.tmp = Path(tempfile.mkdtemp())
|
|
|
|
|
|
self._ctx = _patch_config_dir(self.tmp)
|
|
|
|
|
|
self._ctx.__enter__()
|
|
|
|
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
|
|
self._ctx.__exit__(None, None, None)
|
|
|
|
|
|
shutil.rmtree(str(self.tmp), ignore_errors=True)
|
|
|
|
|
|
|
|
|
|
|
|
def _make_template(self, name, **kwargs):
|
|
|
|
|
|
from core.models import Template
|
|
|
|
|
|
defaults = dict(x_ratio=0.1, y_ratio=0.1, width_ratio=0.3, height_ratio=0.3,
|
|
|
|
|
|
rotation=0.0, type="custom")
|
|
|
|
|
|
defaults.update(kwargs)
|
|
|
|
|
|
return Template(name=name, **defaults)
|
|
|
|
|
|
|
|
|
|
|
|
def test_missing_file_returns_empty(self):
|
|
|
|
|
|
from services.template_service import load_custom_templates
|
|
|
|
|
|
self.assertEqual(load_custom_templates(), [])
|
|
|
|
|
|
|
|
|
|
|
|
def test_add_and_reload(self):
|
|
|
|
|
|
from services.template_service import add_template, load_custom_templates
|
|
|
|
|
|
t = self._make_template("模板A", x_ratio=0.2, y_ratio=0.3,
|
|
|
|
|
|
width_ratio=0.4, height_ratio=0.4, rotation=15.0)
|
|
|
|
|
|
add_template(t)
|
|
|
|
|
|
|
|
|
|
|
|
loaded = load_custom_templates()
|
|
|
|
|
|
self.assertEqual(len(loaded), 1)
|
|
|
|
|
|
lt = loaded[0]
|
|
|
|
|
|
self.assertEqual(lt.name, "模板A")
|
|
|
|
|
|
self.assertAlmostEqual(lt.x_ratio, 0.2)
|
|
|
|
|
|
self.assertAlmostEqual(lt.y_ratio, 0.3)
|
|
|
|
|
|
self.assertAlmostEqual(lt.rotation, 15.0)
|
|
|
|
|
|
self.assertEqual(lt.type, "custom")
|
|
|
|
|
|
|
|
|
|
|
|
def test_add_overwrites_same_name(self):
|
|
|
|
|
|
from services.template_service import add_template, load_custom_templates
|
|
|
|
|
|
add_template(self._make_template("重名", x_ratio=0.1))
|
|
|
|
|
|
add_template(self._make_template("重名", x_ratio=0.9))
|
|
|
|
|
|
|
|
|
|
|
|
loaded = load_custom_templates()
|
|
|
|
|
|
self.assertEqual(len(loaded), 1)
|
|
|
|
|
|
self.assertAlmostEqual(loaded[0].x_ratio, 0.9)
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_all_templates_order(self):
|
|
|
|
|
|
from services.template_service import add_template, get_all_templates, get_builtin_templates
|
|
|
|
|
|
builtin_count = len(get_builtin_templates())
|
|
|
|
|
|
add_template(self._make_template("自定义"))
|
|
|
|
|
|
|
|
|
|
|
|
all_t = get_all_templates()
|
|
|
|
|
|
self.assertEqual(len(all_t), builtin_count + 1)
|
|
|
|
|
|
for t in all_t[:builtin_count]:
|
|
|
|
|
|
self.assertEqual(t.type, "builtin")
|
|
|
|
|
|
self.assertEqual(all_t[-1].type, "custom")
|
|
|
|
|
|
|
|
|
|
|
|
def test_rename_template(self):
|
|
|
|
|
|
from services.template_service import add_template, rename_template, load_custom_templates
|
|
|
|
|
|
add_template(self._make_template("旧名"))
|
|
|
|
|
|
rename_template("旧名", "新名")
|
|
|
|
|
|
loaded = load_custom_templates()
|
|
|
|
|
|
self.assertEqual(loaded[0].name, "新名")
|
|
|
|
|
|
|
|
|
|
|
|
def test_rename_nonexistent_raises_value_error(self):
|
|
|
|
|
|
from services.template_service import rename_template
|
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
|
rename_template("不存在", "随便")
|
|
|
|
|
|
|
|
|
|
|
|
def test_rename_empty_name_raises_value_error(self):
|
|
|
|
|
|
from services.template_service import add_template, rename_template
|
|
|
|
|
|
add_template(self._make_template("有名"))
|
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
|
rename_template("有名", " ")
|
|
|
|
|
|
|
|
|
|
|
|
def test_delete_template(self):
|
|
|
|
|
|
from services.template_service import add_template, delete_template, load_custom_templates
|
|
|
|
|
|
add_template(self._make_template("删我"))
|
|
|
|
|
|
delete_template("删我")
|
|
|
|
|
|
self.assertEqual(load_custom_templates(), [])
|
|
|
|
|
|
|
|
|
|
|
|
def test_delete_nonexistent_raises_value_error(self):
|
|
|
|
|
|
from services.template_service import delete_template
|
|
|
|
|
|
with self.assertRaises(ValueError):
|
|
|
|
|
|
delete_template("不存在")
|
|
|
|
|
|
|
|
|
|
|
|
def test_corrupt_entry_is_skipped_others_load(self):
|
|
|
|
|
|
"""A malformed entry in templates.json must not block valid entries."""
|
|
|
|
|
|
templates_file = self.tmp / "templates.json"
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
"templates": [
|
|
|
|
|
|
{"name": "好的", "x_ratio": 0.1, "y_ratio": 0.1,
|
|
|
|
|
|
"width_ratio": 0.3, "height_ratio": 0.3},
|
|
|
|
|
|
{"name": "坏的"}, # missing width_ratio / height_ratio
|
|
|
|
|
|
{"name": ""}, # empty name
|
|
|
|
|
|
{"name": "零比例", "x_ratio": 0.0, "y_ratio": 0.0,
|
|
|
|
|
|
"width_ratio": 0.0, "height_ratio": 0.0}, # zero ratios
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
templates_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
templates_file.write_text(json.dumps(payload), encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
from services.template_service import load_custom_templates
|
|
|
|
|
|
loaded = load_custom_templates()
|
|
|
|
|
|
self.assertEqual(len(loaded), 1)
|
|
|
|
|
|
self.assertEqual(loaded[0].name, "好的")
|
|
|
|
|
|
|
|
|
|
|
|
def test_corrupt_json_returns_empty(self):
|
|
|
|
|
|
"""Completely invalid JSON in templates.json should return empty list."""
|
|
|
|
|
|
templates_file = self.tmp / "templates.json"
|
|
|
|
|
|
templates_file.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
templates_file.write_text("{not valid json", encoding="utf-8")
|
|
|
|
|
|
|
|
|
|
|
|
from services.template_service import load_custom_templates
|
|
|
|
|
|
self.assertEqual(load_custom_templates(), [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Template model: to_transform_state
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestTemplateToTransformState(unittest.TestCase):
|
|
|
|
|
|
def test_basic_conversion(self):
|
|
|
|
|
|
from core.models import Template
|
|
|
|
|
|
t = Template(
|
|
|
|
|
|
name="t", x_ratio=0.25, y_ratio=0.25,
|
|
|
|
|
|
width_ratio=0.5, height_ratio=0.5, rotation=30.0,
|
|
|
|
|
|
)
|
|
|
|
|
|
state = t.to_transform_state(400, 600)
|
|
|
|
|
|
self.assertAlmostEqual(state.x, 100.0)
|
|
|
|
|
|
self.assertAlmostEqual(state.y, 150.0)
|
|
|
|
|
|
self.assertAlmostEqual(state.width, 200.0)
|
|
|
|
|
|
self.assertAlmostEqual(state.height, 300.0)
|
|
|
|
|
|
self.assertAlmostEqual(state.rotation, 30.0)
|
|
|
|
|
|
|
|
|
|
|
|
def test_zero_rotation(self):
|
|
|
|
|
|
from core.models import Template
|
|
|
|
|
|
t = Template(name="t", x_ratio=0.0, y_ratio=0.0,
|
|
|
|
|
|
width_ratio=1.0, height_ratio=1.0, rotation=0.0)
|
|
|
|
|
|
state = t.to_transform_state(100, 200)
|
|
|
|
|
|
self.assertAlmostEqual(state.rotation, 0.0)
|
|
|
|
|
|
self.assertAlmostEqual(state.width, 100.0)
|
|
|
|
|
|
self.assertAlmostEqual(state.height, 200.0)
|
|
|
|
|
|
|
2026-06-16 15:11:56 +08:00
|
|
|
|
def test_aspect_fit_square_box_wide_print(self):
|
|
|
|
|
|
"""A wide print in a square box fits by width and stays undistorted."""
|
|
|
|
|
|
from core.models import Template
|
|
|
|
|
|
# Box: 200x200 at (100,100) on a 400x400 garment
|
|
|
|
|
|
t = Template(name="t", x_ratio=0.25, y_ratio=0.25,
|
|
|
|
|
|
width_ratio=0.5, height_ratio=0.5)
|
|
|
|
|
|
state = t.to_transform_state(400, 400, print_width=100, print_height=50)
|
|
|
|
|
|
# Wide print (2:1) → limited by width: 200 wide, 100 tall
|
|
|
|
|
|
self.assertAlmostEqual(state.width, 200.0)
|
|
|
|
|
|
self.assertAlmostEqual(state.height, 100.0)
|
|
|
|
|
|
# Aspect ratio preserved
|
|
|
|
|
|
self.assertAlmostEqual(state.width / state.height, 100 / 50)
|
|
|
|
|
|
# Centered in the box (centre at 200,200)
|
|
|
|
|
|
self.assertAlmostEqual(state.x + state.width / 2, 200.0)
|
|
|
|
|
|
self.assertAlmostEqual(state.y + state.height / 2, 200.0)
|
|
|
|
|
|
|
|
|
|
|
|
def test_aspect_fit_square_box_tall_print(self):
|
|
|
|
|
|
"""A tall print in a square box fits by height and stays undistorted."""
|
|
|
|
|
|
from core.models import Template
|
|
|
|
|
|
t = Template(name="t", x_ratio=0.25, y_ratio=0.25,
|
|
|
|
|
|
width_ratio=0.5, height_ratio=0.5)
|
|
|
|
|
|
state = t.to_transform_state(400, 400, print_width=50, print_height=100)
|
|
|
|
|
|
# Tall print (1:2) → limited by height: 100 wide, 200 tall
|
|
|
|
|
|
self.assertAlmostEqual(state.width, 100.0)
|
|
|
|
|
|
self.assertAlmostEqual(state.height, 200.0)
|
|
|
|
|
|
self.assertAlmostEqual(state.x + state.width / 2, 200.0)
|
|
|
|
|
|
self.assertAlmostEqual(state.y + state.height / 2, 200.0)
|
|
|
|
|
|
|
|
|
|
|
|
def test_aspect_fit_never_exceeds_box(self):
|
|
|
|
|
|
"""Fitted print must never exceed the template box bounds."""
|
|
|
|
|
|
from core.models import Template
|
|
|
|
|
|
t = Template(name="t", x_ratio=0.34, y_ratio=0.22,
|
|
|
|
|
|
width_ratio=0.32, height_ratio=0.32)
|
|
|
|
|
|
# Portrait garment, square print
|
|
|
|
|
|
state = t.to_transform_state(800, 1200, print_width=300, print_height=300)
|
|
|
|
|
|
box_w = 0.32 * 800
|
|
|
|
|
|
box_h = 0.32 * 1200
|
|
|
|
|
|
self.assertLessEqual(state.width, box_w + 1e-6)
|
|
|
|
|
|
self.assertLessEqual(state.height, box_h + 1e-6)
|
|
|
|
|
|
# Square print stays square (no stretch despite portrait garment)
|
|
|
|
|
|
self.assertAlmostEqual(state.width, state.height)
|
|
|
|
|
|
|
2026-06-16 09:38:06 +08:00
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# File scanning
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestFileScan(unittest.TestCase):
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
self.tmp = Path(tempfile.mkdtemp())
|
|
|
|
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
|
|
shutil.rmtree(str(self.tmp), ignore_errors=True)
|
|
|
|
|
|
|
|
|
|
|
|
def _png(self, path: Path):
|
|
|
|
|
|
from PIL import Image
|
|
|
|
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
Image.new("RGB", (1, 1)).save(str(path), "PNG")
|
|
|
|
|
|
|
|
|
|
|
|
def test_scans_flat_folder(self):
|
|
|
|
|
|
from services.file_service import scan_image_folder
|
|
|
|
|
|
self._png(self.tmp / "a.png")
|
|
|
|
|
|
self._png(self.tmp / "b.png")
|
|
|
|
|
|
assets = scan_image_folder(self.tmp)
|
|
|
|
|
|
self.assertEqual(len(assets), 2)
|
|
|
|
|
|
|
|
|
|
|
|
def test_recurses_into_subfolders(self):
|
|
|
|
|
|
from services.file_service import scan_image_folder
|
|
|
|
|
|
self._png(self.tmp / "root.png")
|
|
|
|
|
|
self._png(self.tmp / "sub" / "child.png")
|
|
|
|
|
|
self._png(self.tmp / "sub" / "deep" / "deeper.png")
|
|
|
|
|
|
assets = scan_image_folder(self.tmp)
|
|
|
|
|
|
self.assertEqual(len(assets), 3)
|
|
|
|
|
|
|
|
|
|
|
|
def test_skips_unsupported_formats(self):
|
|
|
|
|
|
from services.file_service import scan_image_folder
|
|
|
|
|
|
self._png(self.tmp / "ok.png")
|
|
|
|
|
|
(self.tmp / "ignored.txt").write_text("text")
|
|
|
|
|
|
(self.tmp / "ignored.pdf").write_bytes(b"%PDF")
|
|
|
|
|
|
assets = scan_image_folder(self.tmp)
|
|
|
|
|
|
self.assertEqual(len(assets), 1)
|
|
|
|
|
|
|
|
|
|
|
|
def test_supports_jpg_and_webp_extensions(self):
|
|
|
|
|
|
from services.file_service import scan_image_folder
|
|
|
|
|
|
from PIL import Image
|
|
|
|
|
|
Image.new("RGB", (1, 1)).save(str(self.tmp / "img.jpg"), "JPEG")
|
|
|
|
|
|
Image.new("RGB", (1, 1)).save(str(self.tmp / "img.jpeg"), "JPEG")
|
|
|
|
|
|
# .webp: just check extension recognition; create a renamed png
|
|
|
|
|
|
self._png(self.tmp / "img.png")
|
|
|
|
|
|
assets = scan_image_folder(self.tmp)
|
|
|
|
|
|
self.assertEqual(len(assets), 3)
|
|
|
|
|
|
|
|
|
|
|
|
def test_assets_selected_by_default(self):
|
|
|
|
|
|
from services.file_service import scan_image_folder
|
|
|
|
|
|
self._png(self.tmp / "img.png")
|
|
|
|
|
|
assets = scan_image_folder(self.tmp)
|
|
|
|
|
|
self.assertTrue(assets[0].selected)
|
|
|
|
|
|
|
|
|
|
|
|
def test_nonexistent_dir_raises_oserror(self):
|
|
|
|
|
|
from services.file_service import scan_image_folder
|
|
|
|
|
|
with self.assertRaises(OSError):
|
|
|
|
|
|
scan_image_folder(self.tmp / "nonexistent")
|
|
|
|
|
|
|
|
|
|
|
|
def test_file_path_raises_oserror(self):
|
|
|
|
|
|
from services.file_service import scan_image_folder
|
|
|
|
|
|
f = self.tmp / "f.png"
|
|
|
|
|
|
self._png(f)
|
|
|
|
|
|
with self.assertRaises(OSError):
|
|
|
|
|
|
scan_image_folder(f) # path is a file, not a directory
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Batch pair generation (core.batch._build_pairs)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestBatchPairs(unittest.TestCase):
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
from core.models import BatchMode
|
|
|
|
|
|
from core.batch import _build_pairs
|
|
|
|
|
|
self.build = _build_pairs
|
|
|
|
|
|
self.Mode = BatchMode
|
|
|
|
|
|
self.paths = lambda *names: [Path(n) for n in names]
|
|
|
|
|
|
|
|
|
|
|
|
def test_full_combo_all_combinations(self):
|
|
|
|
|
|
g = self.paths("g1", "g2")
|
|
|
|
|
|
p = self.paths("p1", "p2", "p3")
|
|
|
|
|
|
pairs = self.build(g, p, self.Mode.FULL_COMBO)
|
|
|
|
|
|
self.assertEqual(len(pairs), 6) # 2 × 3
|
|
|
|
|
|
|
|
|
|
|
|
def test_full_combo_order(self):
|
|
|
|
|
|
g = self.paths("g1", "g2")
|
|
|
|
|
|
p = self.paths("p1", "p2")
|
|
|
|
|
|
pairs = self.build(g, p, self.Mode.FULL_COMBO)
|
|
|
|
|
|
expected = [(Path("g1"), Path("p1")), (Path("g1"), Path("p2")),
|
|
|
|
|
|
(Path("g2"), Path("p1")), (Path("g2"), Path("p2"))]
|
|
|
|
|
|
self.assertEqual(pairs, expected)
|
|
|
|
|
|
|
|
|
|
|
|
def test_many_garments_uses_first_print(self):
|
|
|
|
|
|
g = self.paths("g1", "g2", "g3")
|
|
|
|
|
|
p = self.paths("p1", "p2")
|
|
|
|
|
|
pairs = self.build(g, p, self.Mode.MANY_GARMENTS)
|
|
|
|
|
|
self.assertEqual(len(pairs), 3)
|
|
|
|
|
|
self.assertTrue(all(pr[1] == Path("p1") for pr in pairs))
|
|
|
|
|
|
|
|
|
|
|
|
def test_many_garments_empty_prints_returns_empty(self):
|
|
|
|
|
|
g = self.paths("g1", "g2")
|
|
|
|
|
|
pairs = self.build(g, [], self.Mode.MANY_GARMENTS)
|
|
|
|
|
|
self.assertEqual(pairs, [])
|
|
|
|
|
|
|
|
|
|
|
|
def test_many_prints_uses_first_garment(self):
|
|
|
|
|
|
g = self.paths("g1", "g2")
|
|
|
|
|
|
p = self.paths("p1", "p2", "p3")
|
|
|
|
|
|
pairs = self.build(g, p, self.Mode.MANY_PRINTS)
|
|
|
|
|
|
self.assertEqual(len(pairs), 3)
|
|
|
|
|
|
self.assertTrue(all(pr[0] == Path("g1") for pr in pairs))
|
|
|
|
|
|
|
|
|
|
|
|
def test_many_prints_empty_garments_returns_empty(self):
|
|
|
|
|
|
p = self.paths("p1", "p2")
|
|
|
|
|
|
pairs = self.build([], p, self.Mode.MANY_PRINTS)
|
|
|
|
|
|
self.assertEqual(pairs, [])
|
|
|
|
|
|
|
|
|
|
|
|
def test_one_to_one_zips_lists(self):
|
|
|
|
|
|
g = self.paths("g1", "g2", "g3")
|
|
|
|
|
|
p = self.paths("p1", "p2")
|
|
|
|
|
|
pairs = self.build(g, p, self.Mode.ONE_TO_ONE)
|
|
|
|
|
|
self.assertEqual(len(pairs), 2) # zip stops at shorter
|
|
|
|
|
|
self.assertEqual(pairs[0], (Path("g1"), Path("p1")))
|
|
|
|
|
|
self.assertEqual(pairs[1], (Path("g2"), Path("p2")))
|
|
|
|
|
|
|
|
|
|
|
|
def test_empty_inputs_return_empty(self):
|
|
|
|
|
|
pairs = self.build([], [], self.Mode.FULL_COMBO)
|
|
|
|
|
|
self.assertEqual(pairs, [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
# Batch result summary (run_batch, single-failure resilience)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
class TestBatchRunSingleFailure(unittest.TestCase):
|
|
|
|
|
|
"""Verify that run_batch continues even when one item fails."""
|
|
|
|
|
|
|
|
|
|
|
|
def setUp(self):
|
|
|
|
|
|
self.tmp = Path(tempfile.mkdtemp())
|
|
|
|
|
|
|
|
|
|
|
|
def tearDown(self):
|
|
|
|
|
|
shutil.rmtree(str(self.tmp), ignore_errors=True)
|
|
|
|
|
|
|
|
|
|
|
|
def _png(self, name, color=(200, 200, 200)):
|
|
|
|
|
|
from PIL import Image
|
|
|
|
|
|
p = self.tmp / name
|
|
|
|
|
|
Image.new("RGB", (50, 50), color).save(str(p), "PNG")
|
|
|
|
|
|
return p
|
|
|
|
|
|
|
|
|
|
|
|
def test_single_bad_file_does_not_stop_batch(self):
|
|
|
|
|
|
from core.batch import run_batch
|
|
|
|
|
|
from core.models import BatchMode, BatchOptions, ExportOptions, TransformState
|
|
|
|
|
|
|
|
|
|
|
|
good1 = self._png("g1.png", (200, 200, 200))
|
|
|
|
|
|
good2 = self._png("g2.png", (100, 100, 100))
|
|
|
|
|
|
print_img = self._png("p.png", (255, 0, 0))
|
|
|
|
|
|
bad_file = self.tmp / "nonexistent.png"
|
|
|
|
|
|
|
|
|
|
|
|
garments = [good1, bad_file, good2]
|
|
|
|
|
|
prints = [print_img]
|
|
|
|
|
|
transform = TransformState(x=10, y=10, width=20, height=20)
|
|
|
|
|
|
opts = BatchOptions(
|
|
|
|
|
|
mode=BatchMode.MANY_GARMENTS,
|
|
|
|
|
|
export_options=ExportOptions(
|
|
|
|
|
|
output_dir=str(self.tmp / "out"),
|
|
|
|
|
|
output_format="PNG",
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
result = run_batch(garments, prints, transform, opts)
|
|
|
|
|
|
|
|
|
|
|
|
self.assertEqual(result.total, 3)
|
|
|
|
|
|
self.assertEqual(result.success_count, 2)
|
|
|
|
|
|
self.assertEqual(result.failure_count, 1)
|
|
|
|
|
|
self.assertFalse(result.results[1].success) # bad_file entry failed
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
unittest.main()
|