Newer
Older
SmTIAS-LightSim / tests / visualize / test_viewer.py
"""visualize/viewer.py の仕様ベーステスト.

仕様出典:
- Phase 1 実装サマリー(show_scene 仕様)
- docs/01_GUIDE/GUIDE_08_テスト方針.md(可視化テストは smoke テスト)

テスト方針:
- PyVista は必ず off_screen=True で実行する(GUI を開かない)
- 「ファイルが生成されたか」程度の smoke テストで十分(GUIDE_08)
- Mitsuba をモックしない(GUIDE_08)

テスト対象:
    show_scene(scene_config, project_root, irradiance=None, *, off_screen=False, screenshot=None)
"""

from __future__ import annotations

from pathlib import Path

import numpy as np
import pytest

from sm_tias_lightsim.scene.schema import SceneConfig
from sm_tias_lightsim.visualize.baker import bake_irradiance
from sm_tias_lightsim.visualize.viewer import show_scene

# ---------------------------------------------------------------------------
# フィクスチャ:minimal.yaml をそのまま使う
# ---------------------------------------------------------------------------

MINIMAL_YAML = Path(__file__).parent.parent / "fixtures" / "minimal.yaml"


@pytest.fixture(scope="module")
def minimal_scene_config() -> tuple[SceneConfig, Path]:
    """tests/fixtures/minimal.yaml から SceneConfig と project_root を返す.

    module スコープで 1 回だけロード(YAML パース・Pydantic 検証は速いため).
    """
    from sm_tias_lightsim.scene.schema import load_scene_yaml

    config = load_scene_yaml(MINIMAL_YAML)
    project_root = MINIMAL_YAML.parent
    return config, project_root


# ---------------------------------------------------------------------------
# 1. irradiance なしの smoke テスト
# ---------------------------------------------------------------------------


@pytest.mark.slow
def test_show_scene_without_irradiance_creates_screenshot(
    tmp_path: Path,
    minimal_scene_config: tuple[SceneConfig, Path],
):
    """irradiance=None のとき screenshot が生成されること(no-bake モード).

    仕様: irradiance なし → light gray の半透明メッシュで表示
    screenshot 指定時は off_screen=True を強制し,screenshot を保存する
    """
    config, project_root = minimal_scene_config
    screenshot_path = tmp_path / "out_no_irr.png"

    show_scene(
        config,
        project_root,
        irradiance=None,
        screenshot=screenshot_path,
    )

    assert screenshot_path.exists(), "irradiance なしで screenshot ファイルが生成されること"
    assert screenshot_path.stat().st_size > 0, "screenshot ファイルが空でないこと"


# ---------------------------------------------------------------------------
# 2. irradiance ありの smoke テスト
# ---------------------------------------------------------------------------


@pytest.mark.slow
def test_show_scene_with_irradiance_creates_screenshot(
    tmp_path: Path,
    minimal_scene_config: tuple[SceneConfig, Path],
):
    """irradiance を渡したとき screenshot が生成されること(bake モード).

    仕様: irradiance あり → 各メッシュを cell_data["irradiance"] で着色(viridis)
    """
    config, project_root = minimal_scene_config

    # ベイク結果を取得
    irradiance = bake_irradiance(config, project_root)

    screenshot_path = tmp_path / "out_with_irr.png"
    show_scene(
        config,
        project_root,
        irradiance=irradiance,
        screenshot=screenshot_path,
    )

    assert screenshot_path.exists(), "irradiance ありで screenshot ファイルが生成されること"
    assert screenshot_path.stat().st_size > 0, "screenshot ファイルが空でないこと"


# ---------------------------------------------------------------------------
# 3. screenshot=None, off_screen=True は no-op であること(例外を投げない)
# ---------------------------------------------------------------------------


@pytest.mark.slow
def test_show_scene_off_screen_without_screenshot_is_noop(
    tmp_path: Path,
    minimal_scene_config: tuple[SceneConfig, Path],
):
    """screenshot=None かつ off_screen=True のとき例外を投げないこと(no-op).

    仕様: screenshot=None かつ off_screen=True のときは no-op
    (viewer.py show_scene の末尾条件分岐)
    """
    config, project_root = minimal_scene_config

    # 例外が発生しないこと(戻り値は None)
    result = show_scene(
        config,
        project_root,
        irradiance=None,
        off_screen=True,
        screenshot=None,
    )

    assert result is None, "show_scene は None を返すこと"


# ---------------------------------------------------------------------------
# 4. screenshot 指定時に off_screen が強制されること(例外なく完了する)
# ---------------------------------------------------------------------------


@pytest.mark.slow
def test_show_scene_screenshot_forces_off_screen(
    tmp_path: Path,
    minimal_scene_config: tuple[SceneConfig, Path],
):
    """screenshot を指定すると off_screen=False でも off_screen=True に強制されること.

    仕様: screenshot 指定時は off_screen=True を強制し,screenshot を保存する
    GUI ウィンドウが開かないことを間接的に確認する(off_screen 強制は CI/CD 環境で必須).
    """
    config, project_root = minimal_scene_config
    screenshot_path = tmp_path / "forced_offscreen.png"

    # off_screen=False でも screenshot 指定なら強制的に off_screen になるはず
    show_scene(
        config,
        project_root,
        irradiance=None,
        off_screen=False,  # これは screenshot 指定で上書きされる
        screenshot=screenshot_path,
    )

    assert screenshot_path.exists(), "off_screen=False でも screenshot 指定なら保存されること"


# ---------------------------------------------------------------------------
# 5. irradiance の cell_data がメッシュに紐付けられていること(smoke)
# ---------------------------------------------------------------------------


@pytest.mark.slow
def test_show_scene_irradiance_with_wrong_mesh_id_does_not_crash(
    tmp_path: Path,
    minimal_scene_config: tuple[SceneConfig, Path],
):
    """存在しない mesh_id のキーを irradiance に渡しても例外を投げないこと.

    仕様: irradiance が None でないが mesh_id が辞書に含まれない場合,
    そのメッシュは light gray の半透明で表示される(else ブランチ).
    """
    config, project_root = minimal_scene_config
    screenshot_path = tmp_path / "wrong_id.png"

    # 存在しない mesh_id を持つ irradiance を渡す
    fake_irradiance = {"nonexistent_mesh": np.zeros(10, dtype=np.float32)}

    show_scene(
        config,
        project_root,
        irradiance=fake_irradiance,
        screenshot=screenshot_path,
    )

    assert screenshot_path.exists(), "不正な mesh_id でも例外を投げず screenshot が生成されること"