chore: snapshot workflow migration progress
This commit is contained in:
@@ -0,0 +1,574 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import selectors
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_resolve_render_samples_uses_system_settings_when_omitted(monkeypatch):
|
||||
from app.services.render_blender import _resolve_render_samples
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.step_processor._get_all_settings",
|
||||
lambda: {
|
||||
"blender_cycles_samples": "32",
|
||||
"blender_eevee_samples": "12",
|
||||
},
|
||||
)
|
||||
|
||||
assert _resolve_render_samples("cycles", None) == 32
|
||||
assert _resolve_render_samples("eevee", None) == 12
|
||||
assert _resolve_render_samples("cycles", 48) == 48
|
||||
|
||||
|
||||
def test_resolve_tessellation_settings_uses_profile_specific_values(monkeypatch):
|
||||
from app.services.render_blender import resolve_tessellation_settings
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.step_processor._get_all_settings",
|
||||
lambda: {
|
||||
"tessellation_engine": "occ",
|
||||
"scene_linear_deflection": "0.1",
|
||||
"scene_angular_deflection": "0.1",
|
||||
"render_linear_deflection": "0.03",
|
||||
"render_angular_deflection": "0.05",
|
||||
},
|
||||
)
|
||||
|
||||
assert resolve_tessellation_settings("scene") == (0.1, 0.1, "occ")
|
||||
assert resolve_tessellation_settings("render") == (0.03, 0.05, "occ")
|
||||
|
||||
|
||||
def test_render_still_passes_resolved_samples_to_blender_cli(tmp_path, monkeypatch):
|
||||
from app.services.render_blender import build_tessellated_glb_path, render_still
|
||||
|
||||
step_path = tmp_path / "bearing.step"
|
||||
step_path.write_text("STEP", encoding="utf-8")
|
||||
glb_path = build_tessellated_glb_path(step_path, "render", "occ", 0.03, 0.05)
|
||||
glb_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
glb_path.write_text("GLB", encoding="utf-8")
|
||||
output_path = tmp_path / "render.png"
|
||||
output_path.write_text("PNG", encoding="utf-8")
|
||||
|
||||
scripts_dir = tmp_path / "render-scripts"
|
||||
scripts_dir.mkdir()
|
||||
(scripts_dir / "blender_render.py").write_text("# test stub\n", encoding="utf-8")
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
class _FakeProc:
|
||||
def __init__(self) -> None:
|
||||
self.stdout = object()
|
||||
self.stderr = object()
|
||||
self.pid = 1234
|
||||
self.returncode = 0
|
||||
|
||||
def wait(self, timeout: int | None = None) -> int:
|
||||
del timeout
|
||||
return self.returncode
|
||||
|
||||
def wait(self, timeout: int | None = None) -> int:
|
||||
del timeout
|
||||
return self.returncode
|
||||
|
||||
def wait(self, timeout: int = 10) -> int:
|
||||
return self.returncode
|
||||
|
||||
class _FakeSelector:
|
||||
def register(self, *_args, **_kwargs) -> None:
|
||||
return None
|
||||
|
||||
def get_map(self) -> dict:
|
||||
return {}
|
||||
|
||||
def close(self) -> None:
|
||||
return None
|
||||
|
||||
def _fake_popen(cmd, stdout, stderr, text, env, start_new_session):
|
||||
captured["cmd"] = cmd
|
||||
captured["env"] = env
|
||||
return _FakeProc()
|
||||
|
||||
monkeypatch.setenv("RENDER_SCRIPTS_DIR", str(scripts_dir))
|
||||
monkeypatch.setattr("app.services.render_blender.find_blender", lambda: "/usr/bin/blender")
|
||||
monkeypatch.setattr("app.services.render_blender.ensure_group_writable_dir", lambda _path: None)
|
||||
monkeypatch.setattr("app.services.render_blender._resolve_render_samples", lambda engine, samples: 32)
|
||||
monkeypatch.setattr("app.services.render_blender.subprocess.Popen", _fake_popen)
|
||||
monkeypatch.setattr(selectors, "DefaultSelector", _FakeSelector)
|
||||
|
||||
result = render_still(
|
||||
step_path=step_path,
|
||||
output_path=output_path,
|
||||
engine="cycles",
|
||||
samples=None,
|
||||
width=640,
|
||||
height=480,
|
||||
)
|
||||
|
||||
assert captured["cmd"][10] == "32"
|
||||
assert captured["env"]["BLENDER_DEFAULT_SAMPLES"] == "32"
|
||||
assert result["engine_used"] == "cycles"
|
||||
|
||||
|
||||
def test_render_still_passes_template_inputs_to_blender_cli(tmp_path, monkeypatch):
|
||||
from app.services.render_blender import build_tessellated_glb_path, render_still
|
||||
|
||||
step_path = tmp_path / "bearing.step"
|
||||
step_path.write_text("STEP", encoding="utf-8")
|
||||
glb_path = build_tessellated_glb_path(step_path, "render", "occ", 0.03, 0.05)
|
||||
glb_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
glb_path.write_text("GLB", encoding="utf-8")
|
||||
output_path = tmp_path / "render.png"
|
||||
output_path.write_text("PNG", encoding="utf-8")
|
||||
|
||||
scripts_dir = tmp_path / "render-scripts"
|
||||
scripts_dir.mkdir()
|
||||
(scripts_dir / "blender_render.py").write_text("# test stub\n", encoding="utf-8")
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
class _FakeProc:
|
||||
def __init__(self) -> None:
|
||||
self.stdout = object()
|
||||
self.stderr = object()
|
||||
self.pid = 1234
|
||||
self.returncode = 0
|
||||
|
||||
def wait(self, timeout: int = 10) -> int:
|
||||
return self.returncode
|
||||
|
||||
class _FakeSelector:
|
||||
def register(self, *_args, **_kwargs) -> None:
|
||||
return None
|
||||
|
||||
def get_map(self) -> dict:
|
||||
return {}
|
||||
|
||||
def close(self) -> None:
|
||||
return None
|
||||
|
||||
def _fake_popen(cmd, stdout, stderr, text, env, start_new_session):
|
||||
captured["cmd"] = cmd
|
||||
return _FakeProc()
|
||||
|
||||
monkeypatch.setenv("RENDER_SCRIPTS_DIR", str(scripts_dir))
|
||||
monkeypatch.setattr("app.services.render_blender.find_blender", lambda: "/usr/bin/blender")
|
||||
monkeypatch.setattr("app.services.render_blender.ensure_group_writable_dir", lambda _path: None)
|
||||
monkeypatch.setattr("app.services.render_blender._resolve_render_samples", lambda engine, samples: 32)
|
||||
monkeypatch.setattr("app.services.render_blender.subprocess.Popen", _fake_popen)
|
||||
monkeypatch.setattr(selectors, "DefaultSelector", _FakeSelector)
|
||||
|
||||
render_still(
|
||||
step_path=step_path,
|
||||
output_path=output_path,
|
||||
engine="cycles",
|
||||
samples=None,
|
||||
width=640,
|
||||
height=480,
|
||||
template_inputs={"studio_variant": "warm"},
|
||||
)
|
||||
|
||||
assert "--template-inputs" in captured["cmd"]
|
||||
idx = captured["cmd"].index("--template-inputs")
|
||||
assert captured["cmd"][idx + 1] == '{"studio_variant": "warm"}'
|
||||
|
||||
|
||||
def test_render_still_uses_settings_sensitive_render_glb_path(tmp_path, monkeypatch):
|
||||
from app.services.render_blender import build_tessellated_glb_path, render_still
|
||||
|
||||
step_path = tmp_path / "bearing.step"
|
||||
step_path.write_text("STEP", encoding="utf-8")
|
||||
output_path = tmp_path / "render.png"
|
||||
output_path.write_text("PNG", encoding="utf-8")
|
||||
|
||||
scripts_dir = tmp_path / "render-scripts"
|
||||
scripts_dir.mkdir()
|
||||
(scripts_dir / "blender_render.py").write_text("# test stub\n", encoding="utf-8")
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
class _FakeProc:
|
||||
def __init__(self) -> None:
|
||||
self.stdout = object()
|
||||
self.stderr = object()
|
||||
self.pid = 1234
|
||||
self.returncode = 0
|
||||
|
||||
def wait(self, timeout: int = 10) -> int:
|
||||
return self.returncode
|
||||
|
||||
class _FakeSelector:
|
||||
def register(self, *_args, **_kwargs) -> None:
|
||||
return None
|
||||
|
||||
def get_map(self) -> dict:
|
||||
return {}
|
||||
|
||||
def close(self) -> None:
|
||||
return None
|
||||
|
||||
def _fake_glb_from_step(step_path, glb_path, tessellation_engine="occ", tessellation_profile="render"):
|
||||
captured["glb_path"] = glb_path
|
||||
captured["tessellation_engine"] = tessellation_engine
|
||||
captured["tessellation_profile"] = tessellation_profile
|
||||
glb_path.write_text("GLB", encoding="utf-8")
|
||||
|
||||
def _fake_popen(cmd, stdout, stderr, text, env, start_new_session):
|
||||
captured["cmd"] = cmd
|
||||
return _FakeProc()
|
||||
|
||||
monkeypatch.setenv("RENDER_SCRIPTS_DIR", str(scripts_dir))
|
||||
monkeypatch.setattr("app.services.render_blender.find_blender", lambda: "/usr/bin/blender")
|
||||
monkeypatch.setattr("app.services.render_blender.ensure_group_writable_dir", lambda _path: None)
|
||||
monkeypatch.setattr("app.services.render_blender._resolve_render_samples", lambda engine, samples: 32)
|
||||
monkeypatch.setattr(
|
||||
"app.services.step_processor._get_all_settings",
|
||||
lambda: {
|
||||
"tessellation_engine": "occ",
|
||||
"render_linear_deflection": "0.03",
|
||||
"render_angular_deflection": "0.05",
|
||||
"blender_cycles_samples": "32",
|
||||
"blender_eevee_samples": "12",
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr("app.services.render_blender._glb_from_step", _fake_glb_from_step)
|
||||
monkeypatch.setattr("app.services.render_blender.subprocess.Popen", _fake_popen)
|
||||
monkeypatch.setattr(selectors, "DefaultSelector", _FakeSelector)
|
||||
|
||||
render_still(
|
||||
step_path=step_path,
|
||||
output_path=output_path,
|
||||
engine="cycles",
|
||||
samples=None,
|
||||
width=640,
|
||||
height=480,
|
||||
)
|
||||
|
||||
expected_glb_path = build_tessellated_glb_path(step_path, "render", "occ", 0.03, 0.05)
|
||||
assert captured["glb_path"] == expected_glb_path
|
||||
assert captured["tessellation_profile"] == "render"
|
||||
assert captured["cmd"][5] == str(expected_glb_path)
|
||||
|
||||
|
||||
def test_render_turntable_passes_template_inputs_to_blender_cli(tmp_path, monkeypatch):
|
||||
from app.services.render_blender import build_tessellated_glb_path, render_turntable_to_file
|
||||
|
||||
step_path = tmp_path / "bearing.step"
|
||||
step_path.write_text("STEP", encoding="utf-8")
|
||||
glb_path = build_tessellated_glb_path(step_path, "render", "occ", 0.03, 0.05)
|
||||
glb_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
glb_path.write_text("GLB", encoding="utf-8")
|
||||
output_path = tmp_path / "turntable.mp4"
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
scripts_dir = tmp_path / "render-scripts"
|
||||
scripts_dir.mkdir()
|
||||
(scripts_dir / "turntable_render.py").write_text("# test stub\n", encoding="utf-8")
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
class _FakeProc:
|
||||
def __init__(self) -> None:
|
||||
self.pid = 1234
|
||||
self.returncode = 0
|
||||
|
||||
def communicate(self, timeout: int | None = None) -> tuple[str, str]:
|
||||
frames_dir = Path(captured["cmd"][6])
|
||||
frames_dir.mkdir(parents=True, exist_ok=True)
|
||||
(frames_dir / "frame_0001.png").write_text("PNG", encoding="utf-8")
|
||||
return ("[turntable_render] ok\n", "")
|
||||
|
||||
def _fake_popen(cmd, stdout, stderr, text, env, start_new_session):
|
||||
captured["cmd"] = cmd
|
||||
return _FakeProc()
|
||||
|
||||
def _fake_ffmpeg(cmd, capture_output, text, timeout):
|
||||
output_path.write_text("MP4", encoding="utf-8")
|
||||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
|
||||
monkeypatch.setenv("RENDER_SCRIPTS_DIR", str(scripts_dir))
|
||||
monkeypatch.setattr("app.services.render_blender.find_blender", lambda: "/usr/bin/blender")
|
||||
monkeypatch.setattr("app.services.render_blender.ensure_group_writable_dir", lambda _path: None)
|
||||
monkeypatch.setattr("app.services.render_blender.subprocess.Popen", _fake_popen)
|
||||
monkeypatch.setattr("app.services.render_blender.subprocess.run", _fake_ffmpeg)
|
||||
monkeypatch.setattr("app.services.render_blender.build_turntable_ffmpeg_cmd", lambda *args, **kwargs: ["ffmpeg", str(output_path)])
|
||||
monkeypatch.setattr("app.services.render_blender.resolve_tessellation_settings", lambda *args, **kwargs: (0.03, 0.05, "occ"))
|
||||
|
||||
render_turntable_to_file(
|
||||
step_path=step_path,
|
||||
output_path=output_path,
|
||||
engine="cycles",
|
||||
samples=32,
|
||||
template_inputs={"studio_variant": "warm"},
|
||||
)
|
||||
|
||||
assert "--template-inputs" in captured["cmd"]
|
||||
idx = captured["cmd"].index("--template-inputs")
|
||||
assert captured["cmd"][idx + 1] == '{"studio_variant": "warm"}'
|
||||
|
||||
|
||||
def test_render_cinematic_passes_template_inputs_to_blender_cli(tmp_path, monkeypatch):
|
||||
from app.services.render_blender import build_tessellated_glb_path, render_cinematic_to_file
|
||||
|
||||
step_path = tmp_path / "bearing.step"
|
||||
step_path.write_text("STEP", encoding="utf-8")
|
||||
glb_path = build_tessellated_glb_path(step_path, "render", "occ", 0.03, 0.05)
|
||||
glb_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
glb_path.write_text("GLB", encoding="utf-8")
|
||||
output_path = tmp_path / "cinematic.mp4"
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
scripts_dir = tmp_path / "render-scripts"
|
||||
scripts_dir.mkdir()
|
||||
(scripts_dir / "cinematic_render.py").write_text("# test stub\n", encoding="utf-8")
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
class _FakeProc:
|
||||
def __init__(self) -> None:
|
||||
self.stdout = object()
|
||||
self.stderr = object()
|
||||
self.pid = 1234
|
||||
self.returncode = 0
|
||||
|
||||
def wait(self, timeout: int | None = None) -> int:
|
||||
del timeout
|
||||
return self.returncode
|
||||
|
||||
class _FakeSelector:
|
||||
def __init__(self) -> None:
|
||||
self._registered: list[object] = []
|
||||
self._delivered = False
|
||||
|
||||
def register(self, fileobj, _event, data):
|
||||
self._registered.append((fileobj, data))
|
||||
|
||||
def unregister(self, fileobj):
|
||||
self._registered = [item for item in self._registered if item[0] is not fileobj]
|
||||
|
||||
def get_map(self) -> dict[int, object]:
|
||||
return {idx: item for idx, item in enumerate(self._registered)}
|
||||
|
||||
def select(self, timeout=None):
|
||||
del timeout
|
||||
if self._delivered:
|
||||
for fileobj, _data in list(self._registered):
|
||||
if hasattr(fileobj, "readline"):
|
||||
fileobj.readline = lambda: ""
|
||||
self._registered.clear()
|
||||
return []
|
||||
self._delivered = True
|
||||
events = []
|
||||
for fileobj, data in list(self._registered):
|
||||
events.append((SimpleNamespace(fileobj=fileobj, data=data), None))
|
||||
return events
|
||||
|
||||
def close(self):
|
||||
return None
|
||||
|
||||
class _FakeStream:
|
||||
def __init__(self, lines: list[str]) -> None:
|
||||
self._lines = list(lines)
|
||||
|
||||
def readline(self) -> str:
|
||||
if not self._lines:
|
||||
return ""
|
||||
return self._lines.pop(0)
|
||||
|
||||
def _fake_popen(cmd, stdout, stderr, text, env, start_new_session):
|
||||
captured["cmd"] = cmd
|
||||
frames_dir = Path(cmd[6])
|
||||
frames_dir.mkdir(parents=True, exist_ok=True)
|
||||
(frames_dir / "frame_0001.png").write_text("PNG", encoding="utf-8")
|
||||
proc = _FakeProc()
|
||||
proc.stdout = _FakeStream(["[cinematic_render] ok\n"])
|
||||
proc.stderr = _FakeStream([])
|
||||
return proc
|
||||
|
||||
def _fake_ffmpeg(cmd, capture_output, text, timeout):
|
||||
output_path.write_text("MP4", encoding="utf-8")
|
||||
return SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||
|
||||
monkeypatch.setenv("RENDER_SCRIPTS_DIR", str(scripts_dir))
|
||||
monkeypatch.setattr("app.services.render_blender.find_blender", lambda: "/usr/bin/blender")
|
||||
monkeypatch.setattr("app.services.render_blender.ensure_group_writable_dir", lambda _path: None)
|
||||
monkeypatch.setattr("app.services.render_blender.subprocess.Popen", _fake_popen)
|
||||
monkeypatch.setattr("app.services.render_blender.subprocess.run", _fake_ffmpeg)
|
||||
monkeypatch.setattr("app.services.render_blender.build_turntable_ffmpeg_cmd", lambda *args, **kwargs: ["ffmpeg", str(output_path)])
|
||||
monkeypatch.setattr("app.services.render_blender.resolve_tessellation_settings", lambda *args, **kwargs: (0.03, 0.05, "occ"))
|
||||
monkeypatch.setattr("selectors.DefaultSelector", _FakeSelector)
|
||||
|
||||
render_cinematic_to_file(
|
||||
step_path=step_path,
|
||||
output_path=output_path,
|
||||
engine="cycles",
|
||||
samples=32,
|
||||
template_inputs={"studio_variant": "warm"},
|
||||
)
|
||||
|
||||
assert "--template-inputs" in captured["cmd"]
|
||||
idx = captured["cmd"].index("--template-inputs")
|
||||
assert captured["cmd"][idx + 1] == '{"studio_variant": "warm"}'
|
||||
|
||||
|
||||
def test_render_still_task_keeps_samples_unset_until_render_service(tmp_path, monkeypatch):
|
||||
from app.domains.rendering.tasks import render_still_task
|
||||
|
||||
step_path = tmp_path / "bearing.step"
|
||||
step_path.write_text("STEP", encoding="utf-8")
|
||||
output_path = tmp_path / "render.png"
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
def _fake_render_still(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return {"total_duration_s": 0.1}
|
||||
|
||||
monkeypatch.setattr("app.domains.rendering.tasks.log_task_event", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr("app.services.render_blender.render_still", _fake_render_still)
|
||||
|
||||
task_self = SimpleNamespace(
|
||||
request=SimpleNamespace(id="task-still"),
|
||||
retry=lambda *, exc, countdown: (_ for _ in ()).throw(exc),
|
||||
)
|
||||
|
||||
result = render_still_task.run.__func__(task_self, str(step_path), str(output_path))
|
||||
|
||||
assert captured["samples"] is None
|
||||
assert result["total_duration_s"] == 0.1
|
||||
|
||||
|
||||
def test_blender_args_prefers_backend_default_samples_env(monkeypatch):
|
||||
module_path = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "render-worker"
|
||||
/ "scripts"
|
||||
/ "_blender_args.py"
|
||||
)
|
||||
if not module_path.exists():
|
||||
pytest.skip(f"{module_path} not present in this runtime")
|
||||
spec = importlib.util.spec_from_file_location("test_blender_args_module", module_path)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
monkeypatch.setenv("BLENDER_DEFAULT_SAMPLES", "32")
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
[
|
||||
"blender_render.py",
|
||||
"--",
|
||||
"input.glb",
|
||||
"output.png",
|
||||
"512",
|
||||
"512",
|
||||
"cycles",
|
||||
"",
|
||||
],
|
||||
)
|
||||
|
||||
args = module.parse_args()
|
||||
|
||||
assert args.samples == 32
|
||||
|
||||
|
||||
def test_blender_args_parses_template_inputs(monkeypatch):
|
||||
module_path = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "render-worker"
|
||||
/ "scripts"
|
||||
/ "_blender_args.py"
|
||||
)
|
||||
if not module_path.exists():
|
||||
pytest.skip(f"{module_path} not present in this runtime")
|
||||
spec = importlib.util.spec_from_file_location("test_blender_args_module_template_inputs", module_path)
|
||||
assert spec is not None
|
||||
assert spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
|
||||
monkeypatch.setattr(
|
||||
sys,
|
||||
"argv",
|
||||
[
|
||||
"blender_render.py",
|
||||
"--",
|
||||
"input.glb",
|
||||
"output.png",
|
||||
"512",
|
||||
"512",
|
||||
"cycles",
|
||||
"64",
|
||||
"30",
|
||||
"auto",
|
||||
"0",
|
||||
"",
|
||||
"Product",
|
||||
"",
|
||||
"{}",
|
||||
"[]",
|
||||
"0",
|
||||
"0",
|
||||
"0",
|
||||
"0",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"--template-inputs",
|
||||
'{"studio_variant":"warm"}',
|
||||
],
|
||||
)
|
||||
|
||||
args = module.parse_args()
|
||||
|
||||
assert args.template_inputs == {"studio_variant": "warm"}
|
||||
|
||||
|
||||
def test_render_to_file_preserves_explicit_zero_samples(tmp_path, monkeypatch):
|
||||
from app.services.step_processor import render_to_file
|
||||
|
||||
step_path = tmp_path / "bearing.step"
|
||||
step_path.write_text("STEP", encoding="utf-8")
|
||||
output_path = tmp_path / "render.png"
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"app.services.step_processor._get_all_settings",
|
||||
lambda: {
|
||||
"thumbnail_renderer": "blender",
|
||||
"thumbnail_format": "png",
|
||||
"blender_engine": "cycles",
|
||||
"blender_cycles_samples": "32",
|
||||
"blender_eevee_samples": "12",
|
||||
"cycles_device": "auto",
|
||||
"blender_smooth_angle": "30",
|
||||
"tessellation_engine": "occ",
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr("app.services.step_processor.ensure_group_writable_dir", lambda _path: None)
|
||||
monkeypatch.setattr("app.services.render_blender.is_blender_available", lambda: True)
|
||||
|
||||
def _fake_render_still(**kwargs):
|
||||
captured.update(kwargs)
|
||||
kwargs["output_path"].write_text("PNG", encoding="utf-8")
|
||||
return {"total_duration_s": 0.1, "engine_used": kwargs["engine"]}
|
||||
|
||||
monkeypatch.setattr("app.services.render_blender.render_still", _fake_render_still)
|
||||
|
||||
success, render_log = render_to_file(
|
||||
str(step_path),
|
||||
str(output_path),
|
||||
samples=0,
|
||||
)
|
||||
|
||||
assert success is True
|
||||
assert captured["samples"] == 0
|
||||
assert render_log["samples"] == 0
|
||||
Reference in New Issue
Block a user