"""cli/render.py の仕様ベーステスト.
仕様出典:
- docs/03_PLAN/PLAN_02_開発ステップ.md(Step 5: render CLI 完了条件)
- docs/04_SPEC/SPEC_01_アーキテクチャ設計.md(CLI 層の責務)
- docs/01_GUIDE/GUIDE_08_テスト方針.md(テスト方針)
Step 5 完了条件:
`sm-render --scene <yaml> --output <dir>` で PNG と luminance.npy が生成されること
テスト方針:
- CLI の振る舞いを main() を直接呼び出す形で検証する(subprocess 不使用)
- Mitsuba 実行を伴うテストは @pytest.mark.slow を付ける
- 軽量テスト(argparse / エラーハンドリング)は @pytest.mark.slow なし
"""
from __future__ import annotations
from pathlib import Path
import pytest
from sm_tias_lightsim.cli.render import main
# ---------------------------------------------------------------------------
# テスト用定数
# ---------------------------------------------------------------------------
#: tests/fixtures/minimal.yaml の絶対パス
MINIMAL_YAML = Path(__file__).parent.parent / "fixtures" / "minimal.yaml"
# ---------------------------------------------------------------------------
# 1. 必須引数の欠落(軽量 — argparse 動作)
# ---------------------------------------------------------------------------
def test_missing_scene_arg_exits_with_error():
"""--scene を渡さないと SystemExit が発生すること.
仕様: --scene は必須引数(PLAN_02 Step 5「sm-render --scene <yaml>」)
argparse の required=True は SystemExit を発生させる.
"""
with pytest.raises(SystemExit) as exc_info:
main(["--output", "/tmp/out"])
assert exc_info.value.code != 0
def test_missing_output_arg_exits_with_error():
"""--output を渡さないと SystemExit が発生すること.
仕様: --output は必須引数(PLAN_02 Step 5「--output <dir>」)
argparse の required=True は SystemExit を発生させる.
"""
with pytest.raises(SystemExit) as exc_info:
main(["--scene", str(MINIMAL_YAML)])
assert exc_info.value.code != 0
def test_no_args_exits_with_error():
"""引数なしで呼ぶと SystemExit が発生すること.
仕様: --scene と --output の両方が必須
"""
with pytest.raises(SystemExit) as exc_info:
main([])
assert exc_info.value.code != 0
# ---------------------------------------------------------------------------
# 2. --scene が存在しないパス(軽量 — return code 1, stderr メッセージ)
# ---------------------------------------------------------------------------
def test_nonexistent_scene_returns_1(tmp_path: Path, capsys):
"""存在しない YAML パスを渡すと return 1 かつ stderr にエラーメッセージが出ること.
仕様: FileNotFoundError 捕捉 → stderr「シーン YAML ファイルが見つかりません」→ return 1
(cli/render.py 処理フロー ステップ 2)
"""
nonexistent = tmp_path / "does_not_exist.yaml"
result = main(["--scene", str(nonexistent), "--output", str(tmp_path / "out")])
assert result == 1
captured = capsys.readouterr()
assert "シーン YAML ファイルが見つかりません" in captured.err
def test_nonexistent_scene_error_message_contains_path(tmp_path: Path, capsys):
"""存在しない YAML パスを渡したときのエラーメッセージにパス情報が含まれること.
仕様: stderr「エラー: シーン YAML ファイルが見つかりません — ...」
(cli/render.py 処理フロー ステップ 2)
"""
nonexistent = tmp_path / "no_such_scene.yaml"
main(["--scene", str(nonexistent), "--output", str(tmp_path / "out")])
captured = capsys.readouterr()
assert "エラー:" in captured.err
# ---------------------------------------------------------------------------
# 3. --scene が不正な YAML(ValidationError 等)(軽量 — return code 1)
# ---------------------------------------------------------------------------
def test_invalid_yaml_returns_1(invalid_yaml_file: Path, tmp_path: Path, capsys):
"""必須フィールド欠落の YAML を渡すと return 1 が返ること.
仕様: Pydantic ValidationError 等の一般例外 → stderr「シーン YAML の読み込みに失敗しました」
→ return 1(FileNotFoundError と区別される経路)
(cli/render.py 処理フロー ステップ 2)
"""
result = main(["--scene", str(invalid_yaml_file), "--output", str(tmp_path / "out")])
assert result == 1
def test_invalid_yaml_stderr_message(invalid_yaml_file: Path, tmp_path: Path, capsys):
"""不正 YAML のエラーメッセージに「シーン YAML の読み込みに失敗しました」が含まれること.
仕様: 一般例外捕捉 → stderr「エラー: シーン YAML の読み込みに失敗しました — ...」
(cli/render.py 処理フロー ステップ 2)
"""
main(["--scene", str(invalid_yaml_file), "--output", str(tmp_path / "out")])
captured = capsys.readouterr()
assert "シーン YAML の読み込みに失敗しました" in captured.err
def test_invalid_yaml_does_not_show_file_not_found_message(
invalid_yaml_file: Path, tmp_path: Path, capsys
):
"""存在するが不正な YAML に対して「ファイルが見つかりません」メッセージが出ないこと.
仕様: FileNotFoundError と Pydantic ValidationError の経路が明確に分離されていること
(cli/render.py 処理フロー ステップ 2)
"""
main(["--scene", str(invalid_yaml_file), "--output", str(tmp_path / "out")])
captured = capsys.readouterr()
assert "ファイルが見つかりません" not in captured.err
# ---------------------------------------------------------------------------
# 4 & 5 & 6. end-to-end 統合テスト(slow — Mitsuba 実行)
# ---------------------------------------------------------------------------
@pytest.mark.slow
def test_end_to_end_returns_0(tmp_path: Path):
"""minimal.yaml を入力として main() が return 0 を返すこと.
仕様: Step 5 完了条件「sm-render --scene tests/fixtures/minimal.yaml --output <dir> が動く」
(PLAN_02 Step 5 完了条件)
"""
result = main(["--scene", str(MINIMAL_YAML), "--output", str(tmp_path)])
assert result == 0
@pytest.mark.slow
def test_end_to_end_creates_render_png(tmp_path: Path):
"""end-to-end 実行後に render.png が出力ディレクトリに存在すること.
仕様: sm-render は render.png を出力する (cli/render.py 処理フロー ステップ 5)
"""
main(["--scene", str(MINIMAL_YAML), "--output", str(tmp_path)])
assert (tmp_path / "render.png").exists()
@pytest.mark.slow
def test_end_to_end_creates_luminance_npy(tmp_path: Path):
"""end-to-end 実行後に luminance.npy が出力ディレクトリに存在すること.
仕様: sm-render は luminance.npy を出力する (cli/render.py 処理フロー ステップ 6)
"""
main(["--scene", str(MINIMAL_YAML), "--output", str(tmp_path)])
assert (tmp_path / "luminance.npy").exists()
@pytest.mark.slow
def test_end_to_end_stdout_contains_render_complete(tmp_path: Path, capsys):
"""end-to-end 実行後に stdout に「レンダリング完了」が含まれること.
仕様: 成功時 stdout に「レンダリング完了: ...」を出力する (cli/render.py 処理フロー ステップ 6)
"""
main(["--scene", str(MINIMAL_YAML), "--output", str(tmp_path)])
captured = capsys.readouterr()
assert "レンダリング完了" in captured.out
@pytest.mark.slow
def test_end_to_end_stdout_contains_luminance_saved(tmp_path: Path, capsys):
"""end-to-end 実行後に stdout に「輝度マップ保存」が含まれること.
仕様: 成功時 stdout に「輝度マップ保存: ...」を出力する (cli/render.py 処理フロー ステップ 6)
"""
main(["--scene", str(MINIMAL_YAML), "--output", str(tmp_path)])
captured = capsys.readouterr()
assert "輝度マップ保存" in captured.out
# ---------------------------------------------------------------------------
# 5. 短縮引数 -s / -o(slow)
# ---------------------------------------------------------------------------
@pytest.mark.slow
def test_short_args_s_and_o_work(tmp_path: Path):
"""-s と -o の短縮形でも --scene / --output と同等に動くこと.
仕様: --scene/-s, --output/-o の短縮形を提供する (cli/render.py _parse_args)
"""
result = main(["-s", str(MINIMAL_YAML), "-o", str(tmp_path)])
assert result == 0
assert (tmp_path / "render.png").exists()
assert (tmp_path / "luminance.npy").exists()
# ---------------------------------------------------------------------------
# 6. --output ディレクトリの自動作成(slow)
# ---------------------------------------------------------------------------
@pytest.mark.slow
def test_deep_nested_output_dir_is_created_automatically(tmp_path: Path):
"""存在しない深いネストのディレクトリも mkdir(parents=True) で自動作成されること.
仕様: output_dir.mkdir(parents=True, exist_ok=True) で親ディレクトリも含めて作成する
(cli/render.py 処理フロー ステップ 3)
"""
deep_output = tmp_path / "deep" / "nested" / "out"
assert not deep_output.exists()
result = main(["--scene", str(MINIMAL_YAML), "--output", str(deep_output)])
assert result == 0
assert deep_output.exists()
assert (deep_output / "render.png").exists()
assert (deep_output / "luminance.npy").exists()