Newer
Older
SmTIAS-LightSim / tests / cli / test_render_jit_init.py
@rintoHasegawa rintoHasegawa on 28 May 8 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 追加
"""cli/render.py の JIT 初期化統合テスト (Speed-1 仕様).

仕様出典:
- docs/06_TEST/TEST_04_シミュレーション速度最適化履歴.md — Speed-1 セクション
- Phase 1 実装サマリー (cli/render.py の import 順制御仕様)

テスト対象:
    - E1: cli/render.py がモジュール import の段階で mitsuba を import しないこと
    - E1 (動作): llvm_* variant 設定時に ensure_drjit_libllvm_path を呼んでから
                  render_scene を import すること

テスト方針:
    - AST レベルで import 文の存在をチェック(モジュールレベルの mitsuba import 不在を検証)
    - main() の動作は既存 tests/cli/test_render.py でカバー済み
    - ここでは「import 順の設計意図」を仕様として検証する
"""

from __future__ import annotations

import ast
from pathlib import Path

# ---------------------------------------------------------------------------
# E1: cli/render.py がモジュール import 時点で mitsuba を import しないこと
# ---------------------------------------------------------------------------

_CLI_RENDER_PATH = (
    Path(__file__).parent.parent.parent / "src" / "sm_tias_lightsim" / "cli" / "render.py"
)


class TestCliRenderImportOrder:
    """E1: cli/render.py の import 順が仕様通りであることを検証する.

    仕様:
        cli/render.py がモジュール import の段階で mitsuba を import しないこと.
        ensure_drjit_libllvm_path を呼ぶための前提として、mitsuba は遅延 import される必要がある.
    """

    def test_no_top_level_mitsuba_import_in_cli_render(self) -> None:
        """E1: cli/render.py のモジュールレベルに 'import mitsuba' が存在しないこと.

        仕様:
            --help だけ使う場面(Mitsuba 不在の軽量環境)でも起動できるようにするため,
            Mitsuba 依存 import(load_scene_yaml / render_scene 等)は main() 内で遅延 import する.
        """
        src = open(_CLI_RENDER_PATH, encoding="utf-8").read()
        tree = ast.parse(src)

        # トップレベル(関数外)の import 文のみを取得
        top_level_imports: list[ast.Import | ast.ImportFrom] = []
        for node in ast.iter_child_nodes(tree):
            if isinstance(node, (ast.Import, ast.ImportFrom)):
                top_level_imports.append(node)

        # mitsuba を直接 import している行がないこと
        mitsuba_imports = []
        for imp in top_level_imports:
            if isinstance(imp, ast.Import):
                for alias in imp.names:
                    if alias.name.startswith("mitsuba"):
                        mitsuba_imports.append(ast.dump(imp))
            elif isinstance(imp, ast.ImportFrom):
                if imp.module and imp.module.startswith("mitsuba"):
                    mitsuba_imports.append(ast.dump(imp))

        assert not mitsuba_imports, (
            f"cli/render.py のモジュールレベルに mitsuba import が存在する: {mitsuba_imports}\n"
            "仕様: mitsuba はモジュールレベルで import してはならない(遅延 import 必須)"
        )

    def test_no_top_level_render_runner_import_in_cli_render(self) -> None:
        """E1: cli/render.py のモジュールレベルに render.runner の import が存在しないこと.

        仕様:
            runner.py は mitsuba を遅延 import するが,
            cli/render.py がモジュールレベルで runner を import すると
            ensure_drjit_libllvm_path の前に mitsuba import chain が走る可能性がある.
        """
        src = open(_CLI_RENDER_PATH, encoding="utf-8").read()
        tree = ast.parse(src)

        runner_imports = []
        for node in ast.iter_child_nodes(tree):
            if isinstance(node, ast.ImportFrom):
                if node.module and "runner" in node.module:
                    runner_imports.append(ast.dump(node))

        assert not runner_imports, (
            f"cli/render.py のモジュールレベルに runner import が存在する: {runner_imports}\n"
            "仕様: render_scene は main() 内で遅延 import すること"
        )

    def test_only_standard_library_imports_at_top_level(self) -> None:
        """E1: cli/render.py のモジュールレベルには標準ライブラリのみ import されていること.

        仕様:
            モジュール import 段階では標準ライブラリ(argparse / sys / pathlib)のみ使用.
            Mitsuba 依存の内部モジュールは main() 内で遅延 import する.
        """
        src = open(_CLI_RENDER_PATH, encoding="utf-8").read()
        tree = ast.parse(src)

        # 許可される標準ライブラリモジュール名
        allowed_std_modules = frozenset(
            ["__future__", "argparse", "sys", "pathlib", "os", "logging"]
        )

        top_level_imports: list[ast.Import | ast.ImportFrom] = [
            node
            for node in ast.iter_child_nodes(tree)
            if isinstance(node, (ast.Import, ast.ImportFrom))
        ]

        non_std_imports = []
        for imp in top_level_imports:
            if isinstance(imp, ast.Import):
                for alias in imp.names:
                    root = alias.name.split(".")[0]
                    if root not in allowed_std_modules:
                        non_std_imports.append(alias.name)
            elif isinstance(imp, ast.ImportFrom):
                if imp.module:
                    root = imp.module.split(".")[0]
                    if root not in allowed_std_modules:
                        non_std_imports.append(imp.module)

        assert not non_std_imports, (
            f"cli/render.py のモジュールレベルに非標準ライブラリの import がある:"
            f" {non_std_imports}\n"
            "仕様: モジュールレベルは標準ライブラリのみ(Mitsuba / 内部モジュールは遅延 import)"
        )

    def test_ensure_drjit_libllvm_path_is_called_in_main_before_render_scene(self) -> None:
        """E1 (動作): main() の llvm_* 分岐で ensure_drjit_libllvm_path を
           render_scene より前に呼んでいること(AST による検証).

        仕様:
            runner.py の import(build_scene → sensors.py / lights.py → mitsuba)より先に呼ぶ.
            ensure_drjit_libllvm_path は llvm_* variant のとき main() 内で先行呼び出しされる.
        """
        src = open(_CLI_RENDER_PATH, encoding="utf-8").read()
        tree = ast.parse(src)

        # main 関数の AST を取得
        main_func = next(
            (n for n in ast.walk(tree) if isinstance(n, ast.FunctionDef) and n.name == "main"),
            None,
        )
        assert main_func is not None, "main() 関数が cli/render.py に存在すること"

        # main 関数内で ensure_drjit_libllvm_path と render_scene の呼び出し順を確認
        # (AST の lineno ベースで順序を検証)
        ensure_calls: list[int] = []
        render_calls: list[int] = []

        for node in ast.walk(main_func):
            if isinstance(node, ast.Call):
                # ensure_drjit_libllvm_path の呼び出し
                if isinstance(node.func, ast.Name) and node.func.id == "ensure_drjit_libllvm_path":
                    ensure_calls.append(node.lineno)
                elif (
                    isinstance(node.func, ast.Attribute)
                    and node.func.attr == "ensure_drjit_libllvm_path"
                ):
                    ensure_calls.append(node.lineno)
                # render_scene の呼び出し
                if isinstance(node.func, ast.Name) and node.func.id == "render_scene":
                    render_calls.append(node.lineno)
                elif isinstance(node.func, ast.Attribute) and node.func.attr == "render_scene":
                    render_calls.append(node.lineno)

        # ensure_drjit_libllvm_path は main() 内に存在すること
        assert ensure_calls, (
            "main() 内に ensure_drjit_libllvm_path の呼び出しが存在すること\n"
            "仕様: llvm_* variant のとき render_scene より前に呼ぶ"
        )

        # render_scene は main() 内に存在すること
        assert render_calls, "main() 内に render_scene の呼び出しが存在すること"

        # ensure が render より前の行にあること
        assert min(ensure_calls) < min(render_calls), (
            f"ensure_drjit_libllvm_path (line {min(ensure_calls)}) は "
            f"render_scene (line {min(render_calls)}) より前に呼ばれること"
        )