Newer
Older
SmTIAS-LightSim / tests / cli / test_render_variant_override.py
"""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