Newer
Older
SmTIAS-LightSim / tests / visualize / test_baker_jit_init.py
@rintoHasegawa rintoHasegawa on 28 May 12 KB [add] Speed-1 LLVM JIT 自動検出と診断ログ強化 — runtime/llvm_init.py を新規実装.ensure_drjit_libllvm_path() で DRJIT_LIBLLVM_PATH 未設定時に Windows 標準 4 候補 (C:\Program Files\LLVM 等) + POSIX glob + LLVM_DIR + extra_candidates を順次探索して os.environ 設定(既設定なら上書きしない).log_variant_status() で llvm_/cuda_/scalar_ 経路を INFO 明示,要求 llvm_* で実 scalar_* なら WARNING 追加.render/runner.py で variant=llvm_* 時に自動検出を呼び ImportError 時は原因明示ログ→ re-raise,mitsuba を関数内遅延 import 化.visualize/baker.py の _set_variant_once に LLVM 自動探索分岐を追加し direct/path 両方で log_variant_status を呼ぶ.cli/render.py で import 順を制御し sensors.py/lights.py のモジュールレベル mitsuba import 前に検出を確実に通す.docs/02_ENV/ENV_02 に LLVM インストール手順・DRJIT_LIBLLVM_PATH 説明・動作確認コマンドを追記.新規テスト 51 件 (runtime/test_llvm_init.py 25, render/test_runner_jit_init.py 14, visualize/test_baker_jit_init.py 8, cli/test_render_jit_init.py 4) で仕様 A1-E1 完全カバー,DRJIT_LIBLLVM_PATH 設定下で 606 件全通過.速度は不変(目的は再発防止+診断容易化).数値同等性は MC 内部シードで SHA256 異なるが平均相対誤差 7.23e-05 で統計同等性パス.これで環境変数消失によるサイレントクラッシュが再発しない土台が整備された.TEST_04 Speed-1 セクションを実施結果で更新.CLAUDE.md 開発進捗に Speed-1 完了追記+実装済みモジュール一覧に runtime/llvm_init.py 追加
"""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}"
            )