- 状态栏与窗口强制刷新,进度更新无需移动鼠标即可实时显示 - 安装过程拆分为 12 个阶段,每段进度从 0 到 100% - 安装阶段解析 uv 下载事件推进进度,模型下载阶段实时反映字节进度
1603 lines
57 KiB
Python
1603 lines
57 KiB
Python
import importlib
|
|
import inspect
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
from types import ModuleType, SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
SOURCE_ROOT = PROJECT_ROOT / "src"
|
|
|
|
|
|
class AddonTest(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.remove_modules()
|
|
fake_bpy = ModuleType("bpy")
|
|
fake_bpy.props = SimpleNamespace(
|
|
BoolProperty=lambda **options: options,
|
|
EnumProperty=lambda **options: options,
|
|
FloatProperty=lambda **options: options,
|
|
IntProperty=lambda **options: options,
|
|
PointerProperty=lambda **options: options,
|
|
StringProperty=lambda **options: options,
|
|
)
|
|
fake_bpy.types = SimpleNamespace(
|
|
AddonPreferences=object,
|
|
Operator=object,
|
|
Panel=object,
|
|
PropertyGroup=object,
|
|
Scene=type("Scene", (), {}),
|
|
STATUSBAR_HT_header=SimpleNamespace(
|
|
append=lambda draw: cls.status_bar_draws.append(draw),
|
|
remove=lambda draw: cls.status_bar_draws.remove(draw),
|
|
),
|
|
VIEW3D_MT_object_context_menu=SimpleNamespace(
|
|
append=lambda draw: cls.context_menu_draws.append(draw),
|
|
remove=lambda draw: cls.context_menu_draws.remove(draw),
|
|
),
|
|
)
|
|
cls.registered = []
|
|
cls.status_bar_draws = []
|
|
cls.context_menu_draws = []
|
|
fake_bpy.utils = SimpleNamespace(
|
|
register_class=cls.registered.append,
|
|
unregister_class=cls.registered.remove,
|
|
)
|
|
fake_bpy.context = SimpleNamespace(
|
|
preferences=SimpleNamespace(addons={}),
|
|
)
|
|
fake_bpy.app = SimpleNamespace(online_access=True)
|
|
fake_bpy.path = SimpleNamespace(abspath=lambda path: path)
|
|
sys.modules["bpy"] = fake_bpy
|
|
sys.path.insert(0, str(SOURCE_ROOT))
|
|
cls.addon = importlib.import_module("snapform")
|
|
cls.fake_bpy = fake_bpy
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
sys.path.remove(str(SOURCE_ROOT))
|
|
sys.modules.pop("bpy", None)
|
|
cls.remove_modules()
|
|
|
|
@staticmethod
|
|
def remove_modules():
|
|
for module_name in tuple(sys.modules):
|
|
if module_name == "snapform" or module_name.startswith("snapform."):
|
|
sys.modules.pop(module_name)
|
|
|
|
def tearDown(self):
|
|
self.registered.clear()
|
|
self.status_bar_draws.clear()
|
|
self.context_menu_draws.clear()
|
|
self.addon.operators._active_job = None
|
|
self.addon.image_depth._last_depth_plane_model = "BASE"
|
|
if hasattr(self.fake_bpy.types.Scene, "snapform_settings"):
|
|
del self.fake_bpy.types.Scene.snapform_settings
|
|
|
|
def test_uses_extension_manifest_instead_of_legacy_bl_info(self):
|
|
self.assertFalse(hasattr(self.addon, "bl_info"))
|
|
|
|
def test_registers_and_unregisters_extension_types(self):
|
|
self.addon.register()
|
|
|
|
self.assertEqual(self.registered, list(self.addon.CLASSES))
|
|
self.assertTrue(
|
|
hasattr(self.fake_bpy.types.Scene, "snapform_settings")
|
|
)
|
|
self.assertEqual(
|
|
self.status_bar_draws,
|
|
[self.addon.draw_status_bar],
|
|
)
|
|
self.assertEqual(
|
|
self.context_menu_draws,
|
|
[self.addon.draw_image_context_menu],
|
|
)
|
|
|
|
self.addon.unregister()
|
|
|
|
self.assertEqual(self.registered, [])
|
|
self.assertFalse(
|
|
hasattr(self.fake_bpy.types.Scene, "snapform_settings")
|
|
)
|
|
self.assertEqual(self.status_bar_draws, [])
|
|
self.assertEqual(self.context_menu_draws, [])
|
|
|
|
def test_registers_depth_plane_operator_without_cache_rebuild(self):
|
|
self.assertIn(
|
|
self.addon.ConvertImageToDepthPlane,
|
|
self.addon.CLASSES,
|
|
)
|
|
self.assertIn(self.addon.BuildDepthPlane, self.addon.CLASSES)
|
|
self.assertEqual(
|
|
self.addon.BuildDepthPlane.bl_options,
|
|
{"INTERNAL"},
|
|
)
|
|
self.assertFalse(hasattr(self.addon, "ImportDepthPlane"))
|
|
|
|
def test_registers_image_background_operators(self):
|
|
self.assertIn(self.addon.RemoveImageBackground, self.addon.CLASSES)
|
|
self.assertIn(self.addon.ApplyRemovedBackground, self.addon.CLASSES)
|
|
self.assertIn(self.addon.BuildOriginalDepthPlane, self.addon.CLASSES)
|
|
self.assertEqual(
|
|
self.addon.ApplyRemovedBackground.bl_options,
|
|
{"INTERNAL"},
|
|
)
|
|
|
|
def test_completed_depth_job_uses_undoable_build_operator(self):
|
|
source = inspect.getsource(
|
|
self.addon.RunJob._build_depth_plane
|
|
)
|
|
|
|
self.assertIn("bpy.ops.snapform.build_depth_plane(", source)
|
|
self.assertIn("snapform_unshaded=job.unshaded", source)
|
|
self.assertIn(
|
|
"snapform_depth_data_name=job.depth_data_name",
|
|
source,
|
|
)
|
|
self.assertIn('if "FINISHED" not in result:', source)
|
|
self.assertIn(
|
|
'bpy.ops.ed.undo_push(message="Convert to Depth Plane")',
|
|
source,
|
|
)
|
|
self.assertIn('if "FINISHED" not in undo_result:', source)
|
|
|
|
def test_image_context_menu_exposes_depth_and_background_actions(self):
|
|
draw_source = inspect.getsource(
|
|
self.addon.draw_image_context_menu
|
|
)
|
|
|
|
self.assertIn(
|
|
'"snapform.convert_image_to_depth_plane"',
|
|
draw_source,
|
|
)
|
|
self.assertIn(
|
|
'"snapform.remove_image_background"',
|
|
draw_source,
|
|
)
|
|
self.assertIn('text="Remove Background"', draw_source)
|
|
self.assertEqual(draw_source.count("row = self.layout.row()"), 2)
|
|
self.assertIn('operator_context = "INVOKE_DEFAULT"', draw_source)
|
|
self.assertNotIn("import_depth_plane", draw_source)
|
|
self.assertNotIn("Rebuild", draw_source)
|
|
|
|
def test_background_result_creates_new_packed_image(self):
|
|
original_image = SimpleNamespace(
|
|
name="img.png",
|
|
use_fake_user=False,
|
|
)
|
|
transform = object()
|
|
source_object = SimpleNamespace(
|
|
data=original_image,
|
|
matrix_world=transform,
|
|
empty_display_size=2.5,
|
|
empty_image_offset=(-0.25, -0.75),
|
|
)
|
|
pack_calls = []
|
|
result_image = SimpleNamespace(
|
|
name="foreground.png",
|
|
pack=lambda: pack_calls.append(True),
|
|
)
|
|
load_calls = []
|
|
remove_calls = []
|
|
self.fake_bpy.data = SimpleNamespace(
|
|
images=SimpleNamespace(
|
|
load=lambda *args, **options: load_calls.append(
|
|
(args, options)
|
|
)
|
|
or result_image,
|
|
remove=remove_calls.append,
|
|
),
|
|
)
|
|
|
|
with TemporaryDirectory() as directory:
|
|
output_path = Path(directory) / "foreground.png"
|
|
output_path.touch()
|
|
created = self.addon.image_depth.create_removed_background_image(
|
|
source_object,
|
|
output_path,
|
|
)
|
|
|
|
self.assertIs(created, result_image)
|
|
self.assertIs(source_object.data, result_image)
|
|
self.assertIsNot(source_object.data, original_image)
|
|
self.assertTrue(original_image.use_fake_user)
|
|
self.assertIs(source_object.matrix_world, transform)
|
|
self.assertEqual(source_object.empty_display_size, 2.5)
|
|
self.assertEqual(
|
|
source_object.empty_image_offset,
|
|
(-0.25, -0.75),
|
|
)
|
|
self.assertEqual(result_image.name, "img.rb")
|
|
self.assertEqual(pack_calls, [True])
|
|
self.assertEqual(remove_calls, [])
|
|
self.assertEqual(
|
|
load_calls[0][1],
|
|
{"check_existing": False},
|
|
)
|
|
|
|
def test_background_result_name_handles_numbered_format_suffix(self):
|
|
source_object = SimpleNamespace(
|
|
data=SimpleNamespace(name="img.png.001"),
|
|
)
|
|
|
|
self.assertEqual(
|
|
self.addon.image_depth.removed_background_data_name(
|
|
source_object
|
|
),
|
|
"img.001.rb",
|
|
)
|
|
|
|
def test_depth_background_image_does_not_replace_source_empty(self):
|
|
original_image = SimpleNamespace(
|
|
name="img.png",
|
|
use_fake_user=False,
|
|
)
|
|
source_object = SimpleNamespace(data=original_image)
|
|
pack_calls = []
|
|
result_image = SimpleNamespace(
|
|
name="foreground.png",
|
|
pack=lambda: pack_calls.append(True),
|
|
)
|
|
self.fake_bpy.data = SimpleNamespace(
|
|
images=SimpleNamespace(
|
|
load=lambda *_args, **_options: result_image,
|
|
remove=lambda _image: None,
|
|
),
|
|
)
|
|
|
|
with TemporaryDirectory() as directory:
|
|
output_path = Path(directory) / "foreground.png"
|
|
output_path.touch()
|
|
created = self.addon.image_depth.create_removed_background_image(
|
|
source_object,
|
|
output_path,
|
|
assign_to_source=False,
|
|
)
|
|
|
|
self.assertIs(created, result_image)
|
|
self.assertIs(source_object.data, original_image)
|
|
self.assertFalse(original_image.use_fake_user)
|
|
self.assertEqual(result_image.name, "img.rb")
|
|
self.assertEqual(pack_calls, [True])
|
|
|
|
def test_image_empty_background_action_starts_production_job(self):
|
|
source_object = SimpleNamespace(
|
|
name="Reference",
|
|
data=SimpleNamespace(name="img.png"),
|
|
)
|
|
context = SimpleNamespace(object=source_object, scene=object())
|
|
calls = []
|
|
self.fake_bpy.ops = SimpleNamespace(
|
|
snapform=SimpleNamespace(
|
|
run_job=lambda **options: calls.append(options)
|
|
or {"RUNNING_MODAL"},
|
|
),
|
|
)
|
|
operator = self.addon.RemoveImageBackground()
|
|
|
|
with TemporaryDirectory() as directory:
|
|
input_path = Path(directory) / "input.png"
|
|
input_path.touch()
|
|
with patch.object(
|
|
self.addon.image_depth,
|
|
"prepare_image_input",
|
|
return_value=(input_path, True),
|
|
):
|
|
result = operator.execute(context)
|
|
|
|
self.assertEqual(result, {"FINISHED"})
|
|
self.assertEqual(calls[0]["job_type"], "IMAGE_BACKGROUND")
|
|
self.assertEqual(calls[0]["snapform_input_path"], str(input_path))
|
|
self.assertEqual(
|
|
calls[0]["snapform_source_object_name"],
|
|
"Reference",
|
|
)
|
|
self.assertTrue(calls[0]["snapform_delete_input"])
|
|
|
|
def test_original_depth_background_starts_independent_job(self):
|
|
operator = self.addon.ConvertImageToDepthPlane()
|
|
operator.snapform_depth_source = "ORIGINAL"
|
|
operator.snapform_remove_background = True
|
|
operator.snapform_displacement_via = "MODIFIER"
|
|
operator.snapform_unshaded = False
|
|
operator.snapform_subdivision_level = 5
|
|
operator.snapform_delete_original = False
|
|
source_object = SimpleNamespace(
|
|
name="Reference",
|
|
data=SimpleNamespace(name="img.png"),
|
|
)
|
|
context = SimpleNamespace(object=source_object, scene=object())
|
|
calls = []
|
|
self.fake_bpy.ops = SimpleNamespace(
|
|
snapform=SimpleNamespace(
|
|
run_job=lambda **options: calls.append(options)
|
|
or {"RUNNING_MODAL"},
|
|
),
|
|
)
|
|
|
|
with TemporaryDirectory() as directory:
|
|
input_path = Path(directory) / "input.png"
|
|
input_path.touch()
|
|
with patch.object(
|
|
self.addon.image_depth,
|
|
"prepare_image_input",
|
|
return_value=(input_path, False),
|
|
):
|
|
result = operator.execute(context)
|
|
|
|
self.assertEqual(result, {"FINISHED"})
|
|
self.assertEqual(calls[0]["job_type"], "ORIGINAL_BACKGROUND")
|
|
self.assertFalse(calls[0]["snapform_delete_source"])
|
|
|
|
def test_conversion_dialog_exposes_requested_settings(self):
|
|
operator = self.addon.ConvertImageToDepthPlane
|
|
|
|
self.assertNotIn(
|
|
"default",
|
|
operator.__annotations__["snapform_model"],
|
|
)
|
|
self.assertTrue(operator.__annotations__["snapform_use_ray_pose"]["default"])
|
|
self.assertEqual(
|
|
operator.__annotations__["snapform_process_res_factor"]["default"],
|
|
1.0,
|
|
)
|
|
self.assertTrue(
|
|
operator.__annotations__["snapform_delete_original"]["default"]
|
|
)
|
|
self.assertFalse(
|
|
operator.__annotations__["snapform_unshaded"]["default"]
|
|
)
|
|
self.assertFalse(
|
|
operator.__annotations__["snapform_remove_background"]["default"]
|
|
)
|
|
self.assertEqual(
|
|
operator.__annotations__["snapform_subdivision_level"]["default"],
|
|
5,
|
|
)
|
|
self.assertEqual(
|
|
set(operator.__annotations__),
|
|
{
|
|
"snapform_depth_source",
|
|
"snapform_displacement_via",
|
|
"snapform_unshaded",
|
|
"snapform_remove_background",
|
|
"snapform_model",
|
|
"snapform_use_ray_pose",
|
|
"snapform_process_res_factor",
|
|
"snapform_delete_original",
|
|
"snapform_subdivision_level",
|
|
},
|
|
)
|
|
depth_sources = operator.__annotations__["snapform_depth_source"]
|
|
self.assertEqual(depth_sources["default"], "ORIGINAL")
|
|
self.assertEqual(
|
|
[item[0] for item in depth_sources["items"]],
|
|
["ESTIMATE", "ORIGINAL"],
|
|
)
|
|
self.assertEqual(depth_sources["items"][0][1], "AI Generated")
|
|
self.assertEqual(depth_sources["name"], "Depth from")
|
|
displacement_via = operator.__annotations__[
|
|
"snapform_displacement_via"
|
|
]
|
|
self.assertEqual(displacement_via["default"], "MODIFIER")
|
|
self.assertEqual(displacement_via["name"], "Displace via")
|
|
self.assertEqual(
|
|
[item[0] for item in displacement_via["items"]],
|
|
["MODIFIER", "MATERIAL"],
|
|
)
|
|
self.assertEqual(
|
|
operator.__annotations__["snapform_unshaded"]["name"],
|
|
"Unshaded",
|
|
)
|
|
draw_source = inspect.getsource(operator.draw)
|
|
self.assertIn(
|
|
'if self.snapform_depth_source == "ESTIMATE":',
|
|
draw_source,
|
|
)
|
|
self.assertLess(
|
|
draw_source.index(
|
|
'"snapform_process_res_factor",'
|
|
),
|
|
draw_source.index(
|
|
'settings_body.prop(self, "snapform_subdivision_level")'
|
|
),
|
|
)
|
|
self.assertIn(
|
|
'text=f"Resolution Scale ({process_res} px)"',
|
|
draw_source,
|
|
)
|
|
self.assertIn("estimate_box = layout.box()", draw_source)
|
|
self.assertIn(
|
|
'estimate_box.prop(self, "snapform_model")',
|
|
draw_source,
|
|
)
|
|
self.assertIn(
|
|
'estimate_box.prop(self, "snapform_use_ray_pose")',
|
|
draw_source,
|
|
)
|
|
self.assertIn(
|
|
'default_closed=True',
|
|
draw_source,
|
|
)
|
|
self.assertIn('settings_header.label(text="Options")', draw_source)
|
|
self.assertIn('settings_header.label(text="Options")', draw_source)
|
|
self.assertNotIn("layout.separator", draw_source)
|
|
setting_fields = (
|
|
"snapform_displacement_via",
|
|
"snapform_subdivision_level",
|
|
"snapform_remove_background",
|
|
"snapform_unshaded",
|
|
)
|
|
field_positions = [
|
|
draw_source.index(f'settings_body.prop(self, "{field}")')
|
|
for field in setting_fields
|
|
]
|
|
self.assertEqual(field_positions, sorted(field_positions))
|
|
model_items_callback = operator.__annotations__["snapform_model"]["items"]
|
|
with patch.object(
|
|
self.addon.runtime,
|
|
"model_ready",
|
|
return_value=True,
|
|
):
|
|
model_items = model_items_callback(None, None)
|
|
self.assertEqual(
|
|
[item[0] for item in model_items],
|
|
[
|
|
"SMALL",
|
|
"BASE",
|
|
"LARGE_1_1",
|
|
"GIANT_1_1",
|
|
"NESTED_GIANT_LARGE_1_1",
|
|
],
|
|
)
|
|
self.assertIn("invoke_props_dialog", inspect.getsource(operator.invoke))
|
|
self.assertNotIn(
|
|
'"DA3"',
|
|
inspect.getsource(operator),
|
|
)
|
|
|
|
def test_conversion_dialog_remembers_last_depth_model(self):
|
|
operator = self.addon.ConvertImageToDepthPlane()
|
|
operator.properties = SimpleNamespace(
|
|
is_property_set=lambda _name: False,
|
|
)
|
|
context = SimpleNamespace(
|
|
window_manager=SimpleNamespace(
|
|
invoke_props_dialog=lambda invoked, **_options: invoked,
|
|
),
|
|
)
|
|
|
|
self.addon.image_depth._last_depth_plane_model = "GIANT_1_1"
|
|
with patch.object(
|
|
self.addon.runtime,
|
|
"model_ready",
|
|
side_effect=lambda key: key == "GIANT_1_1",
|
|
):
|
|
result = operator.invoke(context, None)
|
|
|
|
self.assertIs(result, operator)
|
|
self.assertEqual(operator.snapform_model, "GIANT_1_1")
|
|
|
|
def test_conversion_dialog_only_lists_installed_models(self):
|
|
items_callback = self.addon.ConvertImageToDepthPlane.__annotations__[
|
|
"snapform_model"
|
|
]["items"]
|
|
with patch.object(
|
|
self.addon.runtime,
|
|
"model_ready",
|
|
side_effect=lambda key: key in {"SMALL", "BASE"},
|
|
):
|
|
items = items_callback(None, None)
|
|
|
|
self.assertEqual([item[0] for item in items], ["SMALL", "BASE"])
|
|
draw_source = inspect.getsource(self.addon.ConvertImageToDepthPlane.draw)
|
|
self.assertIn('text="No DA3 models installed"', draw_source)
|
|
self.assertIn('text="Download one in Host > Models"', draw_source)
|
|
self.assertNotIn("_dialog_requirements_ready", inspect.getsource(self.addon.ConvertImageToDepthPlane))
|
|
|
|
def test_depth_conversion_progress_is_provider_neutral(self):
|
|
starting_source = inspect.getsource(
|
|
self.addon.RunJob._starting_message
|
|
)
|
|
finish_source = inspect.getsource(self.addon.RunJob._finish)
|
|
|
|
self.assertIn('return "Starting depth estimation"', starting_source)
|
|
self.assertIn('"Depth plane is ready"', finish_source)
|
|
self.assertNotIn("DA3 image depth", starting_source)
|
|
self.assertNotIn("DA3 depth plane", finish_source)
|
|
|
|
def test_update_status_formats_stage_progress(self):
|
|
operator = self.addon.RunJob()
|
|
|
|
self.assertEqual(
|
|
operator._format_status(
|
|
{
|
|
"progress": 0.5,
|
|
"message": "Downloaded 3 packages",
|
|
"stage": 3,
|
|
"stages": 12,
|
|
"stage_label": "Install PyTorch",
|
|
},
|
|
"fallback",
|
|
),
|
|
"Step 3/12 Install PyTorch: Downloaded 3 packages",
|
|
)
|
|
self.assertEqual(
|
|
operator._format_status(
|
|
{
|
|
"progress": 0.0,
|
|
"message": "",
|
|
"stage": 10,
|
|
"stages": 12,
|
|
"stage_label": "Download DA3 Small",
|
|
},
|
|
"fallback",
|
|
),
|
|
"Step 10/12 Download DA3 Small",
|
|
)
|
|
self.assertEqual(
|
|
operator._format_status(
|
|
{
|
|
"progress": 0.5,
|
|
"message": "Estimating depth",
|
|
},
|
|
"fallback",
|
|
),
|
|
"Estimating depth",
|
|
)
|
|
|
|
def test_depth_background_is_loaded_without_changing_source_empty(self):
|
|
source = inspect.getsource(self.addon.RunJob._build_depth_plane)
|
|
|
|
self.assertIn("snapform_background_path=", source)
|
|
self.assertNotIn("_apply_removed_background", source)
|
|
|
|
def test_model_options_mark_only_models_that_are_not_downloaded(self):
|
|
items_callback = self.addon.SnapFormSettings.__annotations__["model"][
|
|
"items"
|
|
]
|
|
with patch.object(
|
|
self.addon.runtime,
|
|
"model_ready",
|
|
side_effect=lambda key: key == "BASE",
|
|
):
|
|
items = items_callback(None, None)
|
|
|
|
labels = {item[0]: item[1] for item in items}
|
|
self.assertEqual(labels["BASE"], "DA3 Base")
|
|
self.assertEqual(labels["SMALL"], "DA3 Small (Missing)")
|
|
|
|
def test_packed_image_is_exported_without_unpacking(self):
|
|
operator = self.addon.ConvertImageToDepthPlane()
|
|
operator.snapform_depth_source = "ESTIMATE"
|
|
operator.snapform_model = "GIANT_1_1"
|
|
operator.snapform_use_ray_pose = True
|
|
operator.snapform_process_res_factor = 0.5
|
|
operator.snapform_displacement_via = "MATERIAL"
|
|
operator.snapform_unshaded = True
|
|
operator.snapform_remove_background = True
|
|
operator.snapform_subdivision_level = 5
|
|
operator.snapform_delete_original = True
|
|
image = SimpleNamespace(
|
|
name="img.png",
|
|
filepath="",
|
|
packed_file=object(),
|
|
size=(1920, 1080),
|
|
)
|
|
context = SimpleNamespace(
|
|
object=SimpleNamespace(name="Reference", data=image),
|
|
scene=object(),
|
|
)
|
|
calls = []
|
|
self.fake_bpy.ops = SimpleNamespace(
|
|
snapform=SimpleNamespace(
|
|
run_job=lambda **options: calls.append(options)
|
|
or {"RUNNING_MODAL"},
|
|
),
|
|
)
|
|
|
|
with TemporaryDirectory() as directory:
|
|
packed_path = Path(directory) / "packed.png"
|
|
packed_path.touch()
|
|
with patch.object(
|
|
self.addon.image_depth,
|
|
"save_packed_image",
|
|
return_value=packed_path,
|
|
) as save_packed_image, patch.object(
|
|
self.addon.runtime,
|
|
"model_ready",
|
|
return_value=True,
|
|
):
|
|
result = operator.execute(context)
|
|
|
|
self.assertEqual(result, {"FINISHED"})
|
|
save_packed_image.assert_called_once_with(image, context.scene)
|
|
self.assertEqual(calls[0]["snapform_input_path"], str(packed_path))
|
|
self.assertEqual(calls[0]["snapform_process_res"], 960)
|
|
self.assertEqual(calls[0]["snapform_model_key"], "GIANT_1_1")
|
|
self.assertEqual(
|
|
calls[0]["snapform_displacement_via"],
|
|
"MATERIAL",
|
|
)
|
|
self.assertTrue(calls[0]["snapform_unshaded"])
|
|
self.assertTrue(calls[0]["snapform_remove_background"])
|
|
self.assertEqual(calls[0]["snapform_depth_data_name"], "img.depth")
|
|
self.assertTrue(calls[0]["snapform_delete_input"])
|
|
self.assertEqual(
|
|
self.addon.image_depth._last_depth_plane_model,
|
|
"GIANT_1_1",
|
|
)
|
|
|
|
def test_packed_image_export_writes_a_png_copy(self):
|
|
class PackedImage:
|
|
def save_render(self, filepath, scene):
|
|
self.filepath = filepath
|
|
self.scene = scene
|
|
self.file_format = scene.render.image_settings.file_format
|
|
Path(filepath).write_bytes(b"packed image")
|
|
|
|
image = PackedImage()
|
|
image_settings = SimpleNamespace(file_format="OPEN_EXR")
|
|
scene = SimpleNamespace(
|
|
render=SimpleNamespace(image_settings=image_settings),
|
|
)
|
|
with TemporaryDirectory() as directory:
|
|
output_path = Path(directory) / "input.png"
|
|
with patch.object(
|
|
self.addon.runtime,
|
|
"temporary_input_path",
|
|
return_value=output_path,
|
|
):
|
|
result = self.addon.image_depth.save_packed_image(image, scene)
|
|
|
|
self.assertEqual(result, output_path)
|
|
self.assertEqual(output_path.read_bytes(), b"packed image")
|
|
self.assertEqual(image.filepath, str(output_path))
|
|
self.assertIs(image.scene, scene)
|
|
self.assertEqual(image.file_format, "PNG")
|
|
self.assertEqual(image_settings.file_format, "OPEN_EXR")
|
|
|
|
def test_temporary_packed_input_is_removed_when_job_closes(self):
|
|
closed = []
|
|
with TemporaryDirectory() as directory:
|
|
input_path = Path(directory) / "packed.png"
|
|
input_path.touch()
|
|
self.addon.operators._active_job = SimpleNamespace(
|
|
log_file=SimpleNamespace(close=lambda: closed.append(True)),
|
|
temporary_input_path=input_path,
|
|
)
|
|
|
|
self.addon.operators.close_active_job()
|
|
|
|
self.assertFalse(input_path.exists())
|
|
self.assertEqual(closed, [True])
|
|
self.assertIsNone(self.addon.operators.active_job())
|
|
|
|
def test_image_depth_model_and_ray_pose_are_independent(self):
|
|
settings = SimpleNamespace(
|
|
input_path="debug-input.png",
|
|
export_format="gs_ply",
|
|
model="BASE",
|
|
gs_model="GIANT_1_1",
|
|
)
|
|
operator = self.addon.RunJob()
|
|
operator.job_type = self.addon.operators.IMAGE_DEPTH_JOB
|
|
operator.snapform_source_object_name = "Reference"
|
|
operator.snapform_process_res = 336
|
|
|
|
with TemporaryDirectory() as directory:
|
|
input_path = Path(directory) / "input.png"
|
|
input_path.touch()
|
|
operator.snapform_input_path = str(input_path)
|
|
paths = {
|
|
"log": Path(directory) / "job.log",
|
|
"status": Path(directory) / "job.json",
|
|
"result": Path(directory) / "result",
|
|
}
|
|
for snapform_model, snapform_ray_pose in (
|
|
("SMALL", True),
|
|
("BASE", False),
|
|
("LARGE_1_1", True),
|
|
("GIANT_1_1", False),
|
|
("NESTED_GIANT_LARGE_1_1", True),
|
|
):
|
|
with self.subTest(model=snapform_model), patch.object(
|
|
self.addon.runtime,
|
|
"new_job_paths",
|
|
return_value=paths,
|
|
), patch.object(
|
|
self.addon.runtime,
|
|
"runtime_ready",
|
|
return_value=True,
|
|
), patch.object(
|
|
self.addon.runtime,
|
|
"model_ready",
|
|
return_value=True,
|
|
) as model_ready, patch.object(
|
|
self.addon.runtime,
|
|
"reconstruct_request",
|
|
return_value=self.addon.runtime.HostJobRequest(
|
|
{"job_id": "test"}
|
|
),
|
|
) as reconstruct:
|
|
operator.snapform_model_key = snapform_model
|
|
operator.snapform_use_ray_pose = snapform_ray_pose
|
|
command, prepared_paths = operator._prepare_command(
|
|
None,
|
|
settings,
|
|
)
|
|
|
|
self.assertIsInstance(
|
|
command,
|
|
self.addon.runtime.HostJobRequest,
|
|
)
|
|
self.assertEqual(prepared_paths, paths)
|
|
model_ready.assert_called_once_with(snapform_model)
|
|
self.assertEqual(reconstruct.call_args.args[1], "mini_npz")
|
|
self.assertEqual(
|
|
reconstruct.call_args.kwargs["input_path"],
|
|
input_path,
|
|
)
|
|
self.assertEqual(
|
|
reconstruct.call_args.kwargs["model_key"],
|
|
snapform_model,
|
|
)
|
|
self.assertEqual(
|
|
reconstruct.call_args.kwargs["process_res"],
|
|
336,
|
|
)
|
|
self.assertEqual(
|
|
reconstruct.call_args.kwargs["process_res_method"],
|
|
"upper_bound_resize",
|
|
)
|
|
self.assertEqual(
|
|
reconstruct.call_args.kwargs["use_ray_pose"],
|
|
snapform_ray_pose,
|
|
)
|
|
|
|
def test_image_depth_background_option_requires_ben2(self):
|
|
settings = SimpleNamespace(
|
|
input_path="debug-input.png",
|
|
export_format="mini_npz",
|
|
model="BASE",
|
|
gs_model="GIANT_1_1",
|
|
)
|
|
operator = self.addon.RunJob()
|
|
operator.job_type = self.addon.operators.IMAGE_DEPTH_JOB
|
|
operator.snapform_model_key = "BASE"
|
|
operator.snapform_use_ray_pose = True
|
|
operator.snapform_process_res = 1024
|
|
operator.snapform_remove_background = True
|
|
|
|
with TemporaryDirectory() as directory:
|
|
input_path = Path(directory) / "input.png"
|
|
input_path.touch()
|
|
operator.snapform_input_path = str(input_path)
|
|
with patch.object(
|
|
self.addon.runtime,
|
|
"new_job_paths",
|
|
return_value={
|
|
"log": Path(directory) / "job.log",
|
|
"status": Path(directory) / "job.json",
|
|
"result": Path(directory) / "result",
|
|
},
|
|
), patch.object(
|
|
self.addon.runtime,
|
|
"runtime_ready",
|
|
return_value=True,
|
|
), patch.object(
|
|
self.addon.runtime,
|
|
"model_ready",
|
|
side_effect=lambda key: key == "BASE",
|
|
):
|
|
with self.assertRaisesRegex(RuntimeError, "BEN2 Base"):
|
|
operator._prepare_command(None, settings)
|
|
|
|
def test_image_depth_background_option_reaches_host_request(self):
|
|
runtime = importlib.import_module("snapform.runtime")
|
|
settings = SimpleNamespace(
|
|
input_path="input.png",
|
|
model="BASE",
|
|
gs_model="GIANT_1_1",
|
|
process_res=504,
|
|
process_res_method="upper_bound_resize",
|
|
conf_thresh_percentile=40.0,
|
|
num_max_points=1_000_000,
|
|
show_cameras=True,
|
|
gs_views_interval=1,
|
|
video_frames=16,
|
|
device="AUTO",
|
|
use_ray_pose=False,
|
|
ref_view_strategy="saddle_balanced",
|
|
)
|
|
with patch.object(
|
|
runtime,
|
|
"runtime_python",
|
|
return_value=Path("python"),
|
|
), patch.object(
|
|
runtime,
|
|
"model_directory",
|
|
side_effect=lambda key: Path(key.lower()),
|
|
):
|
|
request = runtime.reconstruct_request(
|
|
settings,
|
|
"mini_npz",
|
|
Path("status.json"),
|
|
Path("result"),
|
|
remove_background=True,
|
|
)
|
|
|
|
self.assertEqual(
|
|
request.payload["parameters"]["remove_background_model_dir"],
|
|
str(Path("ben2_base")),
|
|
)
|
|
|
|
def test_preferences_use_package_identifier(self):
|
|
self.assertEqual(
|
|
self.addon.SnapFormPreferences.bl_idname,
|
|
"snapform",
|
|
)
|
|
|
|
def test_default_storage_is_shared_in_user_home(self):
|
|
runtime = importlib.import_module("snapform.runtime")
|
|
|
|
self.assertEqual(runtime.DEFAULT_STORAGE_ROOT.name, ".snapform")
|
|
self.assertEqual(
|
|
self.addon.SnapFormPreferences.__annotations__["storage_root"][
|
|
"default"
|
|
],
|
|
str(runtime.DEFAULT_STORAGE_ROOT),
|
|
)
|
|
|
|
def test_ai_host_uses_authenticated_loopback_http(self):
|
|
runtime = importlib.import_module("snapform.runtime")
|
|
request_source = inspect.getsource(runtime._host_http_request)
|
|
start_source = inspect.getsource(runtime.start_host)
|
|
|
|
self.assertEqual(runtime.HOST_ADDRESS, "127.0.0.1")
|
|
self.assertIn('"X-SnapForm-Token"', request_source)
|
|
self.assertIn('"/health"', inspect.getsource(runtime._check_host))
|
|
self.assertIn('with_name("ai_host.py")', start_source)
|
|
self.assertIn("ProxyHandler({})", inspect.getsource(runtime))
|
|
self.assertIn('return storage_root() / "host.json"', inspect.getsource(runtime.host_discovery_path))
|
|
|
|
def test_host_jobs_complete_from_status_state(self):
|
|
source = inspect.getsource(self.addon.RunJob.modal)
|
|
|
|
self.assertIn("if job.uses_host:", source)
|
|
self.assertIn('state == "succeeded"', source)
|
|
self.assertIn('{"failed", "cancelled"}', source)
|
|
|
|
def test_process_res_method_defaults_to_upper_bound_resize(self):
|
|
process_res = self.addon.SnapFormSettings.__annotations__[
|
|
"process_res"
|
|
]
|
|
process_res_method = (
|
|
self.addon.SnapFormSettings.__annotations__[
|
|
"process_res_method"
|
|
]
|
|
)
|
|
|
|
self.assertEqual(process_res["default"], 504)
|
|
self.assertEqual(process_res_method["default"], "upper_bound_resize")
|
|
|
|
def test_reconstruct_request_preserves_da3_parameter_names(self):
|
|
runtime = importlib.import_module("snapform.runtime")
|
|
settings = SimpleNamespace(
|
|
input_path="input.png",
|
|
model="GIANT_1_1",
|
|
gs_model="NESTED_GIANT_LARGE_1_1",
|
|
process_res=504,
|
|
process_res_method="upper_bound_resize",
|
|
conf_thresh_percentile=40.0,
|
|
num_max_points=1_000_000,
|
|
show_cameras=True,
|
|
gs_views_interval=1,
|
|
video_frames=16,
|
|
device="CUDA",
|
|
use_ray_pose=False,
|
|
ref_view_strategy="saddle_balanced",
|
|
)
|
|
with patch.object(
|
|
runtime,
|
|
"runtime_python",
|
|
return_value=Path("python"),
|
|
), patch.object(
|
|
runtime,
|
|
"model_directory",
|
|
return_value=Path("model"),
|
|
):
|
|
request = runtime.reconstruct_request(
|
|
settings,
|
|
"gs_ply",
|
|
Path("status.json"),
|
|
Path("result"),
|
|
)
|
|
|
|
parameters = request.payload["parameters"]
|
|
self.assertEqual(request.payload["command"], "reconstruct")
|
|
self.assertEqual(parameters["export_format"], "gs_ply")
|
|
for parameter in (
|
|
"process_res",
|
|
"process_res_method",
|
|
"conf_thresh_percentile",
|
|
"num_max_points",
|
|
"show_cameras",
|
|
"gs_views_interval",
|
|
"use_ray_pose",
|
|
"ref_view_strategy",
|
|
):
|
|
with self.subTest(parameter=parameter):
|
|
self.assertIn(parameter, parameters)
|
|
|
|
def test_reconstruct_request_accepts_image_depth_overrides(self):
|
|
runtime = importlib.import_module("snapform.runtime")
|
|
settings = SimpleNamespace(
|
|
input_path="input.png",
|
|
model="BASE",
|
|
gs_model="GIANT_1_1",
|
|
process_res=504,
|
|
process_res_method="upper_bound_resize",
|
|
conf_thresh_percentile=40.0,
|
|
num_max_points=1_000_000,
|
|
show_cameras=True,
|
|
gs_views_interval=1,
|
|
video_frames=16,
|
|
device="AUTO",
|
|
use_ray_pose=False,
|
|
ref_view_strategy="saddle_balanced",
|
|
)
|
|
with patch.object(
|
|
runtime,
|
|
"runtime_python",
|
|
return_value=Path("python"),
|
|
), patch.object(
|
|
runtime,
|
|
"model_directory",
|
|
return_value=Path("large-model"),
|
|
) as snapform_model_directory:
|
|
request = runtime.reconstruct_request(
|
|
settings,
|
|
"mini_npz",
|
|
Path("status.json"),
|
|
Path("result"),
|
|
model_key="BASE",
|
|
process_res=252,
|
|
process_res_method="lower_bound_resize",
|
|
use_ray_pose=False,
|
|
)
|
|
|
|
snapform_model_directory.assert_called_once_with("BASE")
|
|
parameters = request.payload["parameters"]
|
|
self.assertEqual(parameters["process_res"], 252)
|
|
self.assertEqual(
|
|
parameters["process_res_method"],
|
|
"lower_bound_resize",
|
|
)
|
|
self.assertEqual(parameters["use_ray_pose"], 0)
|
|
|
|
def test_export_format_property_includes_all_da3_formats(self):
|
|
export_format = self.addon.SnapFormSettings.__annotations__[
|
|
"export_format"
|
|
]
|
|
identifiers = [item[0] for item in export_format["items"]]
|
|
|
|
self.assertEqual(
|
|
identifiers,
|
|
["glb", "gs_ply", "depth_vis", "mini_npz"],
|
|
)
|
|
self.assertEqual(export_format["default"], "glb")
|
|
|
|
def test_gs_model_property_only_includes_gaussian_models(self):
|
|
gs_model = self.addon.SnapFormSettings.__annotations__[
|
|
"gs_model"
|
|
]
|
|
identifiers = [item[0] for item in gs_model["items"](None, None)]
|
|
|
|
self.assertEqual(
|
|
identifiers,
|
|
["GIANT_1_1", "NESTED_GIANT_LARGE_1_1"],
|
|
)
|
|
|
|
def test_debug_panel_is_closed_by_default(self):
|
|
self.assertIn(
|
|
"DEFAULT_CLOSED",
|
|
self.addon.DebugPanel.bl_options,
|
|
)
|
|
for panel in (self.addon.Da3DebugPanel, self.addon.Ben2DebugPanel):
|
|
self.assertEqual(panel.bl_parent_id, self.addon.DebugPanel.bl_idname)
|
|
self.assertIn("DEFAULT_CLOSED", panel.bl_options)
|
|
|
|
def test_debug_panel_uses_one_export_button_and_dynamic_settings(self):
|
|
debug_source = inspect.getsource(self.addon.DebugPanel.draw)
|
|
da3_source = inspect.getsource(self.addon.Da3DebugPanel.draw)
|
|
ben2_source = inspect.getsource(self.addon.Ben2DebugPanel.draw)
|
|
|
|
self.assertIn('layout.prop(settings, "device")', debug_source)
|
|
self.assertEqual(da3_source.count("export_row.operator("), 1)
|
|
self.assertIn('settings.export_format == "glb"', da3_source)
|
|
self.assertIn('settings.export_format == "gs_ply"', da3_source)
|
|
self.assertIn('"gs_model" if settings.export_format', da3_source)
|
|
self.assertIn("layout.prop(settings, model_property)", da3_source)
|
|
self.assertNotIn("model_row", da3_source)
|
|
self.assertIn("and model_ready(selected_model_key(settings))", da3_source)
|
|
self.assertNotIn('text="Ready"', da3_source)
|
|
self.assertNotIn('text="Missing"', da3_source)
|
|
self.assertNotIn("License:", da3_source)
|
|
self.assertIn('operator.job_type = "RECONSTRUCT"', da3_source)
|
|
self.assertIn(
|
|
'layout.prop(settings, "background_input_path")',
|
|
ben2_source,
|
|
)
|
|
self.assertIn('text="Remove Background"', ben2_source)
|
|
self.assertIn("and model_ready(BEN2_MODEL.key)", ben2_source)
|
|
self.assertIn('operator.job_type = "REMOVE_BACKGROUND"', ben2_source)
|
|
|
|
def test_host_panel_exposes_shared_host_state_and_controls(self):
|
|
self.assertIn(self.addon.HostPanel, self.addon.CLASSES)
|
|
self.assertIn(self.addon.ControlHost, self.addon.CLASSES)
|
|
source = inspect.getsource(self.addon.HostPanel.draw)
|
|
|
|
self.assertEqual(self.addon.HostPanel.bl_label, "Host")
|
|
self.assertIn('text="Runtime is not installed"', source)
|
|
self.assertIn('text="Install Runtime"', source)
|
|
self.assertIn("runtime.host_status()", source)
|
|
self.assertIn('f"Running (Clients:', source)
|
|
self.assertIn('else "Running (Busy)"', source)
|
|
self.assertIn('"READY": "RADIOBUT_ON"', source)
|
|
self.assertIn('"BUSY": "TIME"', source)
|
|
self.assertNotIn('"READY": "PLAY"', source)
|
|
self.assertNotIn('text=f"Task:', source)
|
|
self.assertIn('elif state == "STOPPED":', source)
|
|
self.assertNotIn("Blender Clients:", source)
|
|
self.assertNotIn("status['port']", source)
|
|
self.assertIn('icon="TRASH"', source)
|
|
self.assertIn('icon="FILE_REFRESH"', source)
|
|
self.assertIn('icon="PAUSE"', source)
|
|
self.assertNotIn('text="Open Log"', source)
|
|
preferences_source = inspect.getsource(self.addon.SnapFormPreferences.draw)
|
|
self.assertIn('text="Open Host Log"', preferences_source)
|
|
|
|
def test_host_start_control_does_not_wait_for_model_runtime(self):
|
|
source = inspect.getsource(self.addon.runtime.enable_host)
|
|
|
|
self.assertIn("start_host()", source)
|
|
self.assertNotIn("ensure_host()", source)
|
|
|
|
def test_ai_operation_restarts_a_manually_stopped_host(self):
|
|
runtime = self.addon.runtime
|
|
connection = runtime.HostConnection(51234, "token", 42, "host")
|
|
health = {
|
|
"host_schema": runtime.HOST_SCHEMA,
|
|
"instance_id": "host",
|
|
"busy": False,
|
|
"clients": 1,
|
|
"cached_models": {},
|
|
}
|
|
previous_auto_start = runtime._host_auto_start
|
|
previous_connection = runtime._host_connection
|
|
previous_snapshot = runtime._host_snapshot
|
|
runtime._host_auto_start = False
|
|
runtime._host_connection = None
|
|
runtime._host_snapshot = {"state": "STOPPED", "message": "Stopped"}
|
|
try:
|
|
with patch.object(
|
|
runtime,
|
|
"_discover_live_host",
|
|
return_value=(connection, health),
|
|
), patch.object(
|
|
runtime,
|
|
"_register_host_client",
|
|
), patch.object(
|
|
runtime,
|
|
"_check_host",
|
|
return_value=health,
|
|
), patch.object(runtime, "_redraw_host_ui"):
|
|
result = runtime.ensure_host(timeout=0.1)
|
|
|
|
self.assertIs(result, connection)
|
|
self.assertTrue(runtime._host_auto_start)
|
|
self.assertEqual(runtime.host_status()["state"], "READY")
|
|
finally:
|
|
runtime._host_auto_start = previous_auto_start
|
|
runtime._host_connection = previous_connection
|
|
runtime._host_snapshot = previous_snapshot
|
|
|
|
def test_blender_startup_does_not_start_ai_host(self):
|
|
runtime = self.addon.runtime
|
|
previous_auto_start = runtime._host_auto_start
|
|
previous_snapshot = runtime._host_snapshot
|
|
runtime._host_auto_start = False
|
|
runtime._host_snapshot = {"state": "STOPPED", "message": "Stopped"}
|
|
try:
|
|
with patch.object(
|
|
runtime,
|
|
"runtime_ready",
|
|
return_value=True,
|
|
), patch.object(
|
|
runtime,
|
|
"_discover_live_host",
|
|
return_value=(None, None),
|
|
), patch.object(
|
|
runtime,
|
|
"_close_finished_launched_host",
|
|
), patch.object(runtime, "start_host") as start_host:
|
|
interval = runtime._service_host_timer()
|
|
|
|
start_host.assert_not_called()
|
|
self.assertEqual(interval, 2.0)
|
|
self.assertEqual(runtime.host_status()["state"], "STOPPED")
|
|
finally:
|
|
runtime._host_auto_start = previous_auto_start
|
|
runtime._host_snapshot = previous_snapshot
|
|
|
|
def test_models_panel_is_collapsed_and_assigns_each_download_target(self):
|
|
draw_source = inspect.getsource(
|
|
self.addon.ModelsPanel.draw
|
|
)
|
|
model_source = inspect.getsource(
|
|
self.addon.ModelsPanel._draw_model
|
|
)
|
|
|
|
self.assertIn("for model in MODELS.values()", draw_source)
|
|
self.assertIn(
|
|
"self._draw_model(layout, BEN2_MODEL, running)",
|
|
draw_source,
|
|
)
|
|
self.assertIn("operator.model_key = model.key", model_source)
|
|
self.assertIn(
|
|
"download_row.enabled = runtime_ready() and not running",
|
|
model_source,
|
|
)
|
|
self.assertNotIn('label(text="Not downloaded"', model_source)
|
|
self.assertNotIn("Isolated Runtime ready", draw_source)
|
|
self.assertNotIn("License:", model_source)
|
|
self.assertEqual(
|
|
self.addon.ModelsPanel.bl_label,
|
|
"Models",
|
|
)
|
|
self.assertEqual(
|
|
self.addon.ModelsPanel.bl_parent_id,
|
|
self.addon.HostPanel.bl_idname,
|
|
)
|
|
self.assertEqual(
|
|
self.addon.ModelsPanel.bl_options,
|
|
{"DEFAULT_CLOSED"},
|
|
)
|
|
|
|
def test_ben2_debug_job_uses_its_image_and_model(self):
|
|
settings = SimpleNamespace(
|
|
background_input_path="input.png",
|
|
device="CUDA",
|
|
)
|
|
operator = self.addon.RunJob()
|
|
operator.job_type = self.addon.operators.BACKGROUND_REMOVAL_JOB
|
|
|
|
with TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
input_path = root / "input.png"
|
|
input_path.touch()
|
|
settings.background_input_path = str(input_path)
|
|
paths = {
|
|
"log": root / "job.log",
|
|
"status": root / "job.json",
|
|
"result": root / "result",
|
|
}
|
|
with patch.object(
|
|
self.addon.runtime,
|
|
"new_job_paths",
|
|
return_value=paths,
|
|
), patch.object(
|
|
self.addon.runtime,
|
|
"runtime_ready",
|
|
return_value=True,
|
|
), patch.object(
|
|
self.addon.runtime,
|
|
"model_ready",
|
|
return_value=True,
|
|
) as model_ready, patch.object(
|
|
self.addon.runtime,
|
|
"remove_background_request",
|
|
return_value=self.addon.runtime.HostJobRequest(
|
|
{"job_id": "test"}
|
|
),
|
|
) as remove_background:
|
|
command, prepared_paths = operator._prepare_command(
|
|
None,
|
|
settings,
|
|
)
|
|
|
|
self.assertIsInstance(command, self.addon.runtime.HostJobRequest)
|
|
self.assertEqual(prepared_paths, paths)
|
|
model_ready.assert_called_once_with("BEN2_BASE")
|
|
self.assertEqual(remove_background.call_args.args[1], input_path)
|
|
self.assertEqual(remove_background.call_args.args[4], "BEN2_BASE")
|
|
|
|
def test_image_background_job_uses_operator_input(self):
|
|
settings = SimpleNamespace(device="AUTO")
|
|
operator = self.addon.RunJob()
|
|
operator.job_type = self.addon.operators.IMAGE_BACKGROUND_JOB
|
|
|
|
with TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
input_path = root / "input.png"
|
|
input_path.touch()
|
|
operator.snapform_input_path = str(input_path)
|
|
paths = {
|
|
"log": root / "job.log",
|
|
"status": root / "job.json",
|
|
"result": root / "result",
|
|
}
|
|
with patch.object(
|
|
self.addon.runtime,
|
|
"new_job_paths",
|
|
return_value=paths,
|
|
), patch.object(
|
|
self.addon.runtime,
|
|
"runtime_ready",
|
|
return_value=True,
|
|
), patch.object(
|
|
self.addon.runtime,
|
|
"model_ready",
|
|
return_value=True,
|
|
), patch.object(
|
|
self.addon.runtime,
|
|
"remove_background_request",
|
|
return_value=self.addon.runtime.HostJobRequest(
|
|
{"job_id": "test"}
|
|
),
|
|
) as remove_background:
|
|
command, prepared_paths = operator._prepare_command(
|
|
None,
|
|
settings,
|
|
)
|
|
|
|
self.assertIsInstance(command, self.addon.runtime.HostJobRequest)
|
|
self.assertEqual(prepared_paths, paths)
|
|
self.assertEqual(remove_background.call_args.args[1], input_path)
|
|
|
|
def test_completed_image_background_job_applies_result_with_undo(self):
|
|
calls = []
|
|
self.fake_bpy.ops = SimpleNamespace(
|
|
snapform=SimpleNamespace(
|
|
apply_removed_background=lambda **options: calls.append(
|
|
("apply", options)
|
|
)
|
|
or {"FINISHED"},
|
|
),
|
|
ed=SimpleNamespace(
|
|
undo_push=lambda **options: calls.append(("undo", options))
|
|
or {"FINISHED"},
|
|
),
|
|
)
|
|
operator = self.addon.RunJob()
|
|
job = SimpleNamespace(
|
|
result_directory=Path("result"),
|
|
source_object_name="Reference",
|
|
)
|
|
|
|
operator._apply_removed_background(job)
|
|
|
|
self.assertEqual(
|
|
calls,
|
|
[
|
|
(
|
|
"apply",
|
|
{
|
|
"snapform_source_object_name": "Reference",
|
|
"snapform_output_path": str(
|
|
Path("result") / "foreground.png"
|
|
),
|
|
},
|
|
),
|
|
("undo", {"message": "Remove Background"}),
|
|
],
|
|
)
|
|
|
|
def test_completed_original_background_job_builds_with_one_undo(self):
|
|
calls = []
|
|
self.fake_bpy.ops = SimpleNamespace(
|
|
snapform=SimpleNamespace(
|
|
build_original_depth_plane=lambda **options: calls.append(
|
|
("build", options)
|
|
)
|
|
or {"FINISHED"},
|
|
),
|
|
ed=SimpleNamespace(
|
|
undo_push=lambda **options: calls.append(("undo", options))
|
|
or {"FINISHED"},
|
|
),
|
|
)
|
|
operator = self.addon.RunJob()
|
|
job = SimpleNamespace(
|
|
result_directory=Path("result"),
|
|
source_object_name="Reference",
|
|
subdivision_level=5,
|
|
delete_source=False,
|
|
displacement_via="MODIFIER",
|
|
unshaded=False,
|
|
)
|
|
|
|
operator._build_original_depth_plane(job)
|
|
|
|
self.assertEqual(calls[0][0], "build")
|
|
self.assertEqual(
|
|
calls[0][1]["snapform_background_path"],
|
|
str(Path("result") / "foreground.png"),
|
|
)
|
|
self.assertFalse(calls[0][1]["snapform_delete_source"])
|
|
self.assertEqual(
|
|
calls[1],
|
|
("undo", {"message": "Convert to Depth Plane"}),
|
|
)
|
|
|
|
def test_ben2_download_command_filters_large_alternative_weights(self):
|
|
runtime = importlib.import_module("snapform.runtime")
|
|
with patch.object(
|
|
runtime,
|
|
"runtime_python",
|
|
return_value=Path("python"),
|
|
), patch.object(
|
|
runtime,
|
|
"model_directory",
|
|
return_value=Path("model"),
|
|
):
|
|
command = runtime.download_model_command(
|
|
"BEN2_BASE",
|
|
Path("status.json"),
|
|
)
|
|
|
|
patterns = [
|
|
command[index + 1]
|
|
for index, value in enumerate(command)
|
|
if value == "--allow-pattern"
|
|
]
|
|
self.assertEqual(patterns, ["config.json", "model.safetensors"])
|
|
|
|
def test_preferences_install_button_is_disabled_while_busy(self):
|
|
draw_source = inspect.getsource(self.addon.SnapFormPreferences.draw)
|
|
|
|
self.assertIn(
|
|
"install_row.enabled = active_job() is None",
|
|
draw_source,
|
|
)
|
|
|
|
def test_completed_export_opens_job_directory(self):
|
|
calls = []
|
|
self.fake_bpy.ops = SimpleNamespace(
|
|
wm=SimpleNamespace(
|
|
path_open=lambda **options: calls.append(options) or {"FINISHED"},
|
|
),
|
|
)
|
|
with TemporaryDirectory() as directory:
|
|
self.addon.operators.open_job_directory(directory)
|
|
|
|
self.assertEqual(len(calls), 1)
|
|
self.assertIn("filepath", calls[0])
|
|
|
|
def test_status_bar_draws_progress_and_cancel_button(self):
|
|
progress_calls = []
|
|
operator_calls = []
|
|
|
|
class Layout:
|
|
ui_units_x = 0.0
|
|
|
|
def row(self, align=False):
|
|
return self
|
|
|
|
def label(self, **_options):
|
|
return None
|
|
|
|
def progress(self, **options):
|
|
progress_calls.append(options)
|
|
|
|
def operator(self, operator, **options):
|
|
operator_calls.append((operator, options))
|
|
|
|
self.addon.operators._active_job = SimpleNamespace()
|
|
context = SimpleNamespace(
|
|
scene=SimpleNamespace(
|
|
snapform_settings=SimpleNamespace(
|
|
status="Processing",
|
|
progress=0.25,
|
|
),
|
|
),
|
|
)
|
|
|
|
self.addon.draw_status_bar(
|
|
SimpleNamespace(layout=Layout()),
|
|
context,
|
|
)
|
|
|
|
self.assertEqual(
|
|
progress_calls,
|
|
[{"factor": 0.25, "type": "BAR", "text": "Processing"}],
|
|
)
|
|
self.assertEqual(
|
|
operator_calls,
|
|
[("snapform.cancel_job", {"text": "", "icon": "X"})],
|
|
)
|
|
|
|
def test_image_depth_status_uses_same_progress_layout(self):
|
|
progress_calls = []
|
|
operator_calls = []
|
|
|
|
class Layout:
|
|
def row(self, align=False):
|
|
return self
|
|
|
|
def label(self, **_options):
|
|
return None
|
|
|
|
def progress(self, **options):
|
|
progress_calls.append(options)
|
|
|
|
def operator(self, operator, **options):
|
|
operator_calls.append((operator, options))
|
|
|
|
self.addon.operators._active_job = SimpleNamespace(
|
|
kind=self.addon.operators.IMAGE_DEPTH_JOB,
|
|
)
|
|
context = SimpleNamespace(
|
|
scene=SimpleNamespace(
|
|
snapform_settings=SimpleNamespace(
|
|
status="Estimating depth",
|
|
progress=0.25,
|
|
),
|
|
),
|
|
)
|
|
|
|
self.addon.draw_status_bar(
|
|
SimpleNamespace(layout=Layout()),
|
|
context,
|
|
)
|
|
|
|
self.assertEqual(
|
|
progress_calls,
|
|
[
|
|
{
|
|
"factor": 0.25,
|
|
"type": "BAR",
|
|
"text": "Estimating depth",
|
|
}
|
|
],
|
|
)
|
|
self.assertEqual(
|
|
operator_calls,
|
|
[("snapform.cancel_job", {"text": "", "icon": "X"})],
|
|
)
|
|
|
|
def test_progress_is_appended_before_cancel_button(self):
|
|
register_source = inspect.getsource(self.addon.register)
|
|
draw_source = inspect.getsource(self.addon.draw_status_bar)
|
|
|
|
self.assertIn("STATUSBAR_HT_header.append", register_source)
|
|
self.assertNotIn("STATUSBAR_HT_header.prepend", register_source)
|
|
self.assertLess(
|
|
draw_source.index("progress_row.progress("),
|
|
draw_source.index('"snapform.cancel_job"'),
|
|
)
|
|
self.assertIn("progress_row.ui_units_x = 12.0", draw_source)
|
|
self.assertNotIn('label(text="SnapForm"', draw_source)
|
|
|
|
def test_ui_redraw_targets_status_bar_and_view_3d_areas(self):
|
|
redrawn = []
|
|
status_regions = (
|
|
SimpleNamespace(
|
|
tag_redraw=lambda: redrawn.append("status-region"),
|
|
),
|
|
)
|
|
areas = (
|
|
SimpleNamespace(
|
|
type="STATUSBAR",
|
|
tag_redraw=lambda: redrawn.append("status"),
|
|
regions=status_regions,
|
|
),
|
|
SimpleNamespace(
|
|
type="VIEW_3D",
|
|
tag_redraw=lambda: redrawn.append("view"),
|
|
regions=(),
|
|
),
|
|
)
|
|
context = SimpleNamespace(
|
|
window_manager=SimpleNamespace(
|
|
windows=(
|
|
SimpleNamespace(screen=SimpleNamespace(areas=areas)),
|
|
),
|
|
),
|
|
)
|
|
|
|
self.addon.status_bar.redraw_ui(context)
|
|
self.addon.status_bar.redraw_ui(context)
|
|
|
|
self.assertEqual(
|
|
redrawn,
|
|
[
|
|
"status",
|
|
"status-region",
|
|
"view",
|
|
"status",
|
|
"status-region",
|
|
"view",
|
|
],
|
|
)
|
|
|
|
def test_force_flag_triggers_window_draw_and_swap(self):
|
|
redrawn = []
|
|
context = SimpleNamespace(
|
|
window_manager=SimpleNamespace(
|
|
windows=(
|
|
SimpleNamespace(
|
|
screen=SimpleNamespace(
|
|
areas=(
|
|
SimpleNamespace(
|
|
type="STATUSBAR",
|
|
tag_redraw=lambda: redrawn.append(
|
|
"status"
|
|
),
|
|
regions=(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
workspace=SimpleNamespace(
|
|
status_text_set_internal=lambda _value: redrawn.append(
|
|
"status-text"
|
|
),
|
|
),
|
|
)
|
|
|
|
with patch.object(
|
|
self.addon.status_bar.bpy,
|
|
"ops",
|
|
SimpleNamespace(
|
|
wm=SimpleNamespace(
|
|
redraw_timer=lambda **options: redrawn.append(
|
|
("swap", options)
|
|
),
|
|
),
|
|
),
|
|
):
|
|
self.addon.status_bar.redraw_ui(context, force=True)
|
|
|
|
self.assertEqual(
|
|
redrawn,
|
|
[
|
|
"status",
|
|
("swap", {"type": "DRAW_WIN_SWAP", "iterations": 1}),
|
|
"status-text",
|
|
],
|
|
)
|
|
|
|
def test_force_flag_ignores_missing_redraw_helpers(self):
|
|
redrawn = []
|
|
context = SimpleNamespace(
|
|
window_manager=SimpleNamespace(
|
|
windows=(
|
|
SimpleNamespace(
|
|
screen=SimpleNamespace(
|
|
areas=(
|
|
SimpleNamespace(
|
|
type="STATUSBAR",
|
|
tag_redraw=lambda: redrawn.append(
|
|
"status"
|
|
),
|
|
regions=(),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
|
|
self.addon.status_bar.redraw_ui(context, force=True)
|
|
|
|
self.assertEqual(redrawn, ["status"])
|