"""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)}) より前に呼ばれること"
)