"""visualize/baker.py の JIT 初期化統合テスト (Speed-1 仕様).
仕様出典:
- docs/06_TEST/TEST_04_シミュレーション速度最適化履歴.md — Speed-1 セクション
- Phase 1 実装サマリー (baker.py の log_variant_status 呼び出し仕様)
テスト対象:
_set_variant_once() 内の JIT 初期化フロー:
- D1: _set_variant_once('scalar_rgb') 成功後に log_variant_status が呼ばれる
- D2: _bake_direct 経由でも variant 設定後にログが出る
- D3: _set_variant_once('llvm_ad_rgb') で ensure_drjit_libllvm_path が呼ばれる(Speed-2 前準備)
テスト方針:
- Mitsuba をモックして軽量テスト(実 bake は slow テストでカバー済み)
- caplog / mock で診断ログとフック呼び出しを検証
- _set_variant_once は内部 API だが Speed-1 仕様の核となる関数なので直接テスト対象とする
"""
from __future__ import annotations
import logging
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
import sm_tias_lightsim.visualize.baker as baker_module
from sm_tias_lightsim.visualize.baker import _set_variant_once
# ---------------------------------------------------------------------------
# フィクスチャ
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def reset_mitsuba_variant_mock():
"""各テストで mitsuba をモック化してクリーンな状態を保つ.
_set_variant_once は内部で mitsuba.variant() / mitsuba.set_variant() を呼ぶため、
テスト用のモック mi を sys.modules に注入する.
"""
# conftest.py で scalar_rgb が設定済みのため、テスト内でのバリアント状態は
# モック mi.variant() の戻り値で制御する
yield
# ---------------------------------------------------------------------------
# D1: _set_variant_once('scalar_rgb') 成功後に log_variant_status が呼ばれる
# ---------------------------------------------------------------------------
class TestSetVariantOnceCallsLogVariantStatus:
"""D1: _set_variant_once 成功後に log_variant_status が呼ばれること."""
def test_scalar_variant_calls_log_variant_status(self) -> None:
"""D1: _set_variant_once('scalar_rgb') 成功後に log_variant_status が呼ばれること.
仕様:
_set_variant_once 内で mi.set_variant が成功したら
log_variant_status(_logger, desired_variant, mi.variant()) を呼ぶ.
"""
dummy_mi = MagicMock()
# variant() が None を返す(未設定状態)→ set_variant を呼ぶ分岐に入る
dummy_mi.variant.return_value = None
with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
with patch("sm_tias_lightsim.visualize.baker.log_variant_status") as mock_log:
_set_variant_once("scalar_rgb")
mock_log.assert_called_once()
def test_log_variant_status_receives_scalar_variant(self) -> None:
"""D1: log_variant_status に 'scalar_rgb' が渡されること.
仕様: _set_variant_once の desired_variant と mi.variant() 戻り値が log に渡される.
"""
dummy_mi = MagicMock()
dummy_mi.variant.return_value = None
# set_variant 呼び出し後に variant() が 'scalar_rgb' を返すように設定
def variant_side_effect() -> str | None:
# set_variant が呼ばれた後は 'scalar_rgb' を返す
if dummy_mi.set_variant.called:
return "scalar_rgb"
return None
dummy_mi.variant.side_effect = variant_side_effect
with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
with patch("sm_tias_lightsim.visualize.baker.log_variant_status") as mock_log:
_set_variant_once("scalar_rgb")
if mock_log.call_args is not None:
_logger_arg, requested, actual = mock_log.call_args[0]
assert requested == "scalar_rgb", (
f"requested が 'scalar_rgb' であること: got {requested}"
)
def test_already_same_variant_does_not_call_log(self) -> None:
"""D1: 既に同じ variant が設定済みのとき log_variant_status が呼ばれないこと.
仕様: 既に同じ variant が設定されている場合は何もしない(log も出さない).
"""
dummy_mi = MagicMock()
# variant() が既に 'scalar_rgb' を返す(設定済み)
dummy_mi.variant.return_value = "scalar_rgb"
with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
with patch("sm_tias_lightsim.visualize.baker.log_variant_status") as mock_log:
_set_variant_once("scalar_rgb")
mock_log.assert_not_called()
def test_already_different_variant_emits_warning_not_log_variant_status(
self, caplog: pytest.LogCaptureFixture
) -> None:
"""D1: 既に異なる variant が設定済みのとき警告が出て log_variant_status は呼ばれないこと.
仕様: 異なる variant が設定されている場合は警告を出して続行する(変更しない).
"""
dummy_mi = MagicMock()
dummy_mi.variant.return_value = "llvm_ad_rgb" # 既設定(異なる)
with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
with patch("sm_tias_lightsim.visualize.baker.log_variant_status") as mock_log:
with caplog.at_level(logging.WARNING, logger="sm_tias_lightsim.visualize.baker"):
_set_variant_once("scalar_rgb")
mock_log.assert_not_called()
warning_records = [r for r in caplog.records if r.levelno >= logging.WARNING]
assert warning_records, "異なる variant 設定済みのとき WARNING が出ること"
# ---------------------------------------------------------------------------
# D2: _bake_direct 経由でも variant 設定後にログが出る
# ---------------------------------------------------------------------------
class TestBakeDirectCallsLogVariantStatus:
"""D2: _bake_direct 経由(bake_irradiance mode='direct')でも variant ログが出ること.
bake_irradiance(mode='direct') は内部で mi.set_variant('scalar_rgb') を呼び、
成功後に log_variant_status を呼ぶ仕様.
"""
def test_bake_direct_variant_log_is_emitted_on_success(
self, caplog: pytest.LogCaptureFixture
) -> None:
"""D2: bake_irradiance(mode='direct') が variant 設定後にログを出すこと.
仕様:
mode='direct' の分岐で _variant_set_succeeded のとき
log_variant_status(_logger, desired_variant, mi.variant()) が呼ばれる.
"""
from sm_tias_lightsim.scene.schema import SceneConfig
# 最小限のシーン設定(実ファイル不要 — build_scene をモック)
data = {
"name": "test_bake_direct_log",
"geometry": {
"meshes": [{"id": "m1", "path": "/fake/m1.stl", "unit": "mm", "material": "white"}]
},
"materials": {"white": {"type": "diffuse", "reflectance": 0.9}},
"lights": [
{
"id": "led",
"type": "point",
"position": [0, 0, 100],
"intensity": 1.0,
"profile": "lambertian",
}
],
"sensor": {
"type": "perspective",
"position": [0, 0, 200],
"target": [0, 0, 0],
"up": [0, 1, 0],
"fov_deg": 60.0,
"film": {"width": 4, "height": 4},
},
"render": {"variant": "scalar_rgb", "integrator": "path", "max_depth": 2, "spp": 1},
}
config = SceneConfig.model_validate(data)
dummy_mi = MagicMock()
dummy_mi.variant.return_value = "scalar_rgb"
dummy_scene = MagicMock()
with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
# build_scene は baker.py 内で遅延 import されるため元モジュールをモックする
# (from sm_tias_lightsim.scene.builder import build_scene)
with patch(
"sm_tias_lightsim.scene.builder.build_scene",
return_value=({"type": "scene"}, "/tmp/fake"),
):
with patch.object(dummy_mi, "load_dict", return_value=dummy_scene):
with patch("shutil.rmtree"):
with patch(
"sm_tias_lightsim.visualize.baker.log_variant_status"
) as mock_log:
with patch(
"sm_tias_lightsim.visualize.baker._bake_direct",
return_value={},
):
with caplog.at_level(
logging.INFO, logger="sm_tias_lightsim.visualize.baker"
):
try:
baker_module.bake_irradiance(
config,
Path("/tmp"),
mode="direct",
)
except Exception:
pass
# log_variant_status が呼ばれたこと(_variant_set_succeeded == True のとき)
mock_log.assert_called()
# ---------------------------------------------------------------------------
# D3: _set_variant_once('llvm_ad_rgb') で ensure_drjit_libllvm_path が呼ばれる(Speed-2 前準備)
# ---------------------------------------------------------------------------
class TestSetVariantOnceLlvmCallsEnsurePath:
"""D3: _set_variant_once('llvm_ad_rgb') で ensure_drjit_libllvm_path が呼ばれること.
仕様(Speed-2 wavefront 化前準備):
llvm_* variant 要求時は DRJIT_LIBLLVM_PATH を自動探索して設定する.
scalar_* / cuda_* は LLVM-C.dll 不要なので探索しない.
"""
def test_llvm_variant_calls_ensure_drjit_libllvm_path(self) -> None:
"""D3: _set_variant_once('llvm_ad_rgb') のとき ensure_drjit_libllvm_path が呼ばれること.
仕様:
Speed-2 wavefront 化用の前準備:
llvm_* variant の場合は DRJIT_LIBLLVM_PATH を自動探索する.
"""
dummy_mi = MagicMock()
dummy_mi.variant.return_value = None # 未設定 → set_variant 分岐に入る
with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
with patch("sm_tias_lightsim.visualize.baker.ensure_drjit_libllvm_path") as mock_ensure:
with patch("sm_tias_lightsim.visualize.baker.log_variant_status"):
_set_variant_once("llvm_ad_rgb")
mock_ensure.assert_called_once()
def test_scalar_variant_does_not_call_ensure_drjit_libllvm_path(self) -> None:
"""D3: _set_variant_once('scalar_rgb') のとき ensure_drjit_libllvm_path が呼ばれないこと.
仕様: scalar_* / cuda_* は LLVM-C.dll 不要なので探索しない.
"""
dummy_mi = MagicMock()
dummy_mi.variant.return_value = None
with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
with patch("sm_tias_lightsim.visualize.baker.ensure_drjit_libllvm_path") as mock_ensure:
with patch("sm_tias_lightsim.visualize.baker.log_variant_status"):
_set_variant_once("scalar_rgb")
mock_ensure.assert_not_called()
def test_llvm_variant_ensure_called_before_set_variant(self) -> None:
"""D3: ensure_drjit_libllvm_path は mi.set_variant より前に呼ばれること.
仕様: mitsuba の import 前に os.environ を設定することが必須.
"""
dummy_mi = MagicMock()
dummy_mi.variant.return_value = None
call_order: list[str] = []
def ensure_side_effect() -> None:
call_order.append("ensure")
def set_variant_side_effect(v: str) -> None:
call_order.append(f"set_variant:{v}")
dummy_mi.set_variant.side_effect = set_variant_side_effect
with patch.dict("sys.modules", {"mitsuba": dummy_mi}):
with patch(
"sm_tias_lightsim.visualize.baker.ensure_drjit_libllvm_path",
side_effect=ensure_side_effect,
):
with patch("sm_tias_lightsim.visualize.baker.log_variant_status"):
_set_variant_once("llvm_ad_rgb")
if "ensure" in call_order and any("set_variant" in c for c in call_order):
ensure_idx = call_order.index("ensure")
set_variant_idx = next(i for i, c in enumerate(call_order) if "set_variant" in c)
assert ensure_idx < set_variant_idx, (
f"ensure は set_variant より先に呼ばれること: {call_order}"
)