"""cli/render.py の variant 上書き(--variant / SM_RENDER_VARIANT)の仕様ベーステスト.
仕様出典:
- docs/02_ENV/ENV_04_開発コマンド.md(sm-render の --variant・環境変数 SM_RENDER_VARIANT)
- 実装サマリー 仕様 7: 優先順位は --variant > SM_RENDER_VARIANT > YAML の render.variant.
上書き時に「variant を上書き: A → B」を表示する
- 実装サマリー 仕様 8: SM_DRJIT_THREADS の不正値の扱い
テスト方針:
- 軽量テストでは render_scene を差し替えて「どの variant で呼ばれたか」だけを確かめる
(variant は runner 内でプロセスに 1 度しか設定できないため,実レンダでは検証できない)
- 実レンダを伴う確認は slow に 1 本置く
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
import pytest
from sm_tias_lightsim.cli.render import main
from sm_tias_lightsim.scene.schema import load_scene_yaml
MINIMAL_YAML = Path(__file__).parent.parent / "fixtures" / "minimal.yaml"
YAML_VARIANT = "llvm_ad_rgb" # minimal.yaml の render.variant
@pytest.fixture
def render_calls(monkeypatch):
"""render_scene を差し替え,渡された SceneConfig を記録する."""
import sm_tias_lightsim.render.runner as runner
import sm_tias_lightsim.runtime.llvm_init as llvm_init
calls: list[dict] = []
def fake_render_scene(config, project_root=None):
calls.append({"config": config, "project_root": project_root})
return np.full((4, 4, 3), 0.5, dtype=np.float32)
monkeypatch.setattr(runner, "render_scene", fake_render_scene)
monkeypatch.setattr(llvm_init, "ensure_drjit_libllvm_path", lambda *a, **k: None)
monkeypatch.delenv("SM_RENDER_VARIANT", raising=False)
return calls
def _run(tmp_path: Path, *extra: str) -> int:
return main(["--scene", str(MINIMAL_YAML), "--output", str(tmp_path / "out"), *extra])
def test_fixture_variant_precondition():
assert load_scene_yaml(MINIMAL_YAML).render.variant == YAML_VARIANT
def test_yaml_variant_is_used_without_override(tmp_path, render_calls, capsys):
assert _run(tmp_path) == 0
assert render_calls[0]["config"].render.variant == YAML_VARIANT
assert "variant を上書き" not in capsys.readouterr().out
assert (tmp_path / "out" / "render.png").is_file()
assert (tmp_path / "out" / "luminance.npy").is_file()
def test_env_overrides_yaml(tmp_path, render_calls, capsys, monkeypatch):
monkeypatch.setenv("SM_RENDER_VARIANT", "scalar_rgb")
assert _run(tmp_path) == 0
assert render_calls[0]["config"].render.variant == "scalar_rgb"
assert f"variant を上書き: {YAML_VARIANT} → scalar_rgb" in capsys.readouterr().out
def test_cli_overrides_yaml(tmp_path, render_calls, capsys):
assert _run(tmp_path, "--variant", "cuda_ad_rgb") == 0
assert render_calls[0]["config"].render.variant == "cuda_ad_rgb"
assert f"variant を上書き: {YAML_VARIANT} → cuda_ad_rgb" in capsys.readouterr().out
def test_cli_overrides_env(tmp_path, render_calls, capsys, monkeypatch):
monkeypatch.setenv("SM_RENDER_VARIANT", "scalar_rgb")
assert _run(tmp_path, "--variant", "cuda_ad_rgb") == 0
assert render_calls[0]["config"].render.variant == "cuda_ad_rgb"
assert f"variant を上書き: {YAML_VARIANT} → cuda_ad_rgb" in capsys.readouterr().out
@pytest.mark.parametrize("blank", ["", " "])
def test_blank_env_is_ignored(tmp_path, render_calls, capsys, monkeypatch, blank):
monkeypatch.setenv("SM_RENDER_VARIANT", blank)
assert _run(tmp_path) == 0
assert render_calls[0]["config"].render.variant == YAML_VARIANT
assert "variant を上書き" not in capsys.readouterr().out
def test_same_variant_is_not_reported_as_override(tmp_path, render_calls, capsys):
assert _run(tmp_path, "--variant", YAML_VARIANT) == 0
assert render_calls[0]["config"].render.variant == YAML_VARIANT
assert "variant を上書き" not in capsys.readouterr().out
def test_override_keeps_other_settings(tmp_path, render_calls):
"""上書きするのは variant だけ(spp・max_depth・シーン内容は YAML のまま)."""
assert _run(tmp_path, "--variant", "cuda_ad_rgb") == 0
original = load_scene_yaml(MINIMAL_YAML)
config = render_calls[0]["config"]
assert config.render.model_dump(exclude={"variant"}) == original.render.model_dump(
exclude={"variant"}
)
assert config.model_dump(exclude={"render"}) == original.model_dump(exclude={"render"})
assert render_calls[0]["project_root"] == MINIMAL_YAML.resolve().parent
def test_yaml_file_is_not_modified(tmp_path, render_calls, monkeypatch):
before = MINIMAL_YAML.read_bytes()
monkeypatch.setenv("SM_RENDER_VARIANT", "scalar_rgb")
assert _run(tmp_path, "--variant", "cuda_ad_rgb") == 0
assert MINIMAL_YAML.read_bytes() == before
def test_help_mentions_variant(capsys):
with pytest.raises(SystemExit) as exc:
main(["--help"])
assert exc.value.code == 0
out = capsys.readouterr().out
assert "--variant" in out
assert "SM_RENDER_VARIANT" in out
def test_invalid_thread_env_fails_render_with_message(tmp_path, capsys, monkeypatch):
"""SM_DRJIT_THREADS が不正なら(黙って全コアで回さず)レンダ失敗として 1 を返すこと."""
monkeypatch.delenv("SM_RENDER_VARIANT", raising=False)
monkeypatch.setenv("SM_DRJIT_THREADS", "abc")
assert _run(tmp_path, "--variant", "scalar_rgb") == 1
assert "SM_DRJIT_THREADS" in capsys.readouterr().err
@pytest.mark.slow
def test_end_to_end_with_variant_option(tmp_path, capsys, monkeypatch):
"""--variant 付きでも実レンダが完走し,PNG と luminance.npy が出ること."""
monkeypatch.delenv("SM_RENDER_VARIANT", raising=False)
monkeypatch.delenv("SM_DRJIT_THREADS", raising=False)
assert _run(tmp_path, "--variant", "scalar_rgb") == 0
assert (tmp_path / "out" / "render.png").is_file()
assert (tmp_path / "out" / "luminance.npy").is_file()
assert f"variant を上書き: {YAML_VARIANT} → scalar_rgb" in capsys.readouterr().out