"""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 すること(処理本体が render/pipeline.py の
render_yaml へ移ったため,順序の検証対象もそちら)
テスト方針:
- 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 しないこと
# ---------------------------------------------------------------------------
_SRC = Path(__file__).parent.parent.parent / "src" / "sm_tias_lightsim"
_CLI_RENDER_PATH = _SRC / "cli" / "render.py"
_PIPELINE_PATH = _SRC / "render" / "pipeline.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_main_delegates_rendering_to_render_yaml(self) -> None:
"""E1: main() は処理本体を render_yaml に委譲していること.
仕様:
cli/render.py は引数のパースと例外 → 終了コード変換だけを担い,
「YAML 読込 → variant 解決 → レンダ → 保存」は render/pipeline.py が行う.
JIT 初期化(ensure_drjit_libllvm_path)の順序保証もそちらへ移っている.
"""
src = open(_CLI_RENDER_PATH, encoding="utf-8").read()
tree = ast.parse(src)
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 に存在すること"
called = {
node.func.id
for node in ast.walk(main_func)
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name)
}
assert "render_yaml" in called, (
"main() は render_yaml を呼ぶこと(レンダ処理は render/pipeline.py に集約する)"
)
class TestRenderPipelineImportOrder:
"""E1: render/pipeline.py の import 順が仕様通りであることを検証する.
仕様:
render_yaml() は llvm_* variant のとき ensure_drjit_libllvm_path を
render_scene(= mitsuba import chain)より前に呼ぶ.
そのためにモジュールレベルでは mitsuba 依存モジュールを import しない.
"""
def test_no_top_level_mitsuba_or_runner_import_in_pipeline(self) -> None:
"""E1: render/pipeline.py のモジュールレベルに mitsuba / runner の import が無いこと.
仕様:
モジュールレベルで import すると DRJIT_LIBLLVM_PATH 設定より前に
LLVM 初期化が走ってしまう(cli/render.py と同じ遅延 import 方針).
"""
src = open(_PIPELINE_PATH, encoding="utf-8").read()
tree = ast.parse(src)
offenders: list[str] = []
for node in ast.iter_child_nodes(tree):
if isinstance(node, ast.Import):
for alias in node.names:
if alias.name.startswith("mitsuba"):
offenders.append(alias.name)
elif isinstance(node, ast.ImportFrom) and node.module:
if node.module.startswith("mitsuba") or "runner" in node.module:
offenders.append(node.module)
assert not offenders, (
f"render/pipeline.py のモジュールレベルに遅延すべき import がある: {offenders}."
"仕様: mitsuba 依存 import はすべて render_yaml() 内で行う"
)
def test_ensure_drjit_libllvm_path_is_called_before_render_scene(self) -> None:
"""E1 (動作): render_yaml() の llvm_* 分岐で ensure_drjit_libllvm_path を
render_scene より前に呼んでいること(AST による検証).
仕様:
runner.py の import(build_scene → sensors.py / lights.py → mitsuba)より先に呼ぶ.
"""
src = open(_PIPELINE_PATH, encoding="utf-8").read()
tree = ast.parse(src)
func = next(
(
n
for n in ast.walk(tree)
if isinstance(n, ast.FunctionDef) and n.name == "render_yaml"
),
None,
)
assert func is not None, "render_yaml() 関数が render/pipeline.py に存在すること"
ensure_calls: list[int] = []
render_calls: list[int] = []
for node in ast.walk(func):
if not isinstance(node, ast.Call):
continue
name = None
if isinstance(node.func, ast.Name):
name = node.func.id
elif isinstance(node.func, ast.Attribute):
name = node.func.attr
if name == "ensure_drjit_libllvm_path":
ensure_calls.append(node.lineno)
elif name == "render_scene":
render_calls.append(node.lineno)
assert ensure_calls, (
"render_yaml() 内に ensure_drjit_libllvm_path の呼び出しが存在すること."
"仕様: llvm_* variant のとき render_scene より前に呼ぶ"
)
assert render_calls, "render_yaml() 内に render_scene の呼び出しが存在すること"
assert min(ensure_calls) < min(render_calls), (
f"ensure_drjit_libllvm_path (line {min(ensure_calls)}) は "
f"render_scene (line {min(render_calls)}) より前に呼ばれること"
)