"""scripts/sweep_blend_bsdf.py の _generate_tmp_yaml 仕様ベーステスト.
仕様出典: 実装サマリー §E
- E1: weight/alpha パラメータが一時 YAML に正しく書き込まれること
- E2: geometry.meshes の STL パスが絶対パスに変換されること
- E3: 一時 YAML が BOM なし UTF-8 で書かれること
- E4: name フィールドが "baseline_minitias_blend_{job_name}" 形式に更新されること
テスト戦略:
- subprocess(sm-render 実行)は含まない._generate_tmp_yaml の純変換ロジックのみをテストする
- 実際の TEMPLATE_YAML(configs/scene_baseline_blend.yaml)を使用する
- 出力先は tmp_path フィクスチャ(実ファイル)を使用する(GUIDE_08 ファイル I/O はモックしない方針)
"""
from __future__ import annotations
import sys
from pathlib import Path
from types import ModuleType
from unittest import mock
import pytest
import yaml
# ---------------------------------------------------------------------------
# sweep_blend_bsdf モジュールをインポートするヘルパ
# ---------------------------------------------------------------------------
_ROOT = Path(__file__).resolve().parents[2]
_SCRIPT_PATH = _ROOT / "scripts" / "sweep_blend_bsdf.py"
_TEMPLATE_YAML = _ROOT / "configs" / "scene_baseline_blend.yaml"
def _load_sweep_module() -> ModuleType:
"""sweep_blend_bsdf.py を直接インポートする.
scripts/ は通常のパッケージではないため importlib.util で直接ロードする.
モジュールキャッシュに登録して再 import を防ぐ.
"""
import importlib.util
spec = importlib.util.spec_from_file_location("sweep_blend_bsdf", _SCRIPT_PATH)
assert spec is not None, f"スクリプトが見つかりません: {_SCRIPT_PATH}"
module = importlib.util.module_from_spec(spec)
# モジュールキャッシュに登録(2回目以降の import を安全にする)
sys.modules["sweep_blend_bsdf"] = module
assert spec.loader is not None
spec.loader.exec_module(module) # type: ignore[union-attr]
return module
@pytest.fixture(scope="module")
def sweep_module() -> ModuleType:
"""sweep_blend_bsdf モジュールを返すフィクスチャ(モジュールスコープ)."""
return _load_sweep_module()
# ---------------------------------------------------------------------------
# テンプレート YAML の存在確認(前提条件チェック)
# ---------------------------------------------------------------------------
def test_template_yaml_exists():
"""configs/scene_baseline_blend.yaml が存在すること(テストの前提条件).
このファイルがなければ E 系テスト全体が無効になる.
"""
assert _TEMPLATE_YAML.exists(), (
f"テンプレート YAML が見つかりません: {_TEMPLATE_YAML}\n"
"configs/scene_baseline_blend.yaml を生成してからテストを実行してください."
)
# ---------------------------------------------------------------------------
# E1: weight/alpha が一時 YAML に正しく書き込まれる
# ---------------------------------------------------------------------------
class TestGenerateTmpYamlParameters:
"""E1 系: weight と alpha が一時 YAML の正しいパスに書き込まれること."""
@pytest.fixture(autouse=True)
def _patch_tmp_dir(self, tmp_path, sweep_module):
"""TMP_DIR を tmp_path に差し替えて一時 YAML の出力先をテスト用にする."""
with mock.patch.object(sweep_module, "TMP_DIR", tmp_path):
yield
def test_weight_written_to_printer_white(self, sweep_module):
"""生成された YAML の materials.printer_white.weight が入力値 0.15 と一致すること.
仕様 E1: weight=0.15 が一時 YAML の printer_white.weight に書き込まれる
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
assert data["materials"]["printer_white"]["weight"] == pytest.approx(0.15)
def test_alpha_written_to_roughconductor_component(self, sweep_module):
"""生成された YAML の components[1].alpha が入力値 0.3 と一致すること.
仕様 E1: alpha=0.3 が components[1].alpha に書き込まれる
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
assert data["materials"]["printer_white"]["components"][1]["alpha"] == pytest.approx(0.3)
def test_weight_0_05_written_correctly(self, sweep_module):
"""weight=0.05 が正しく書き込まれること(スイープ最小値).
仕様 E1: スイープグリッドの全値で正しく書き込まれること
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.05, 0.1)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
assert data["materials"]["printer_white"]["weight"] == pytest.approx(0.05)
def test_alpha_0_6_written_correctly(self, sweep_module):
"""alpha=0.6 が正しく書き込まれること(スイープ最大値).
仕様 E1: スイープグリッドの全値で正しく書き込まれること
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.30, 0.6)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
assert data["materials"]["printer_white"]["components"][1]["alpha"] == pytest.approx(0.6)
def test_weight_0_30_written_correctly(self, sweep_module):
"""weight=0.30 が正しく書き込まれること(スイープ最大値).
仕様 E1: スイープグリッドの全値で正しく書き込まれること
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.30, 0.3)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
assert data["materials"]["printer_white"]["weight"] == pytest.approx(0.30)
def test_component0_type_still_diffuse_after_write(self, sweep_module):
"""components[0].type が "diffuse" のまま保持されること(破壊的書き換えがないこと).
仕様 E1: alpha 書き換えは components[1] のみに影響する
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
assert data["materials"]["printer_white"]["components"][0]["type"] == "diffuse"
def test_component1_type_still_roughconductor_after_write(self, sweep_module):
"""components[1].type が "roughconductor" のまま保持されること.
仕様 E1: alpha 書き換えは値のみで type フィールドは変更しない
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
assert data["materials"]["printer_white"]["components"][1]["type"] == "roughconductor"
# ---------------------------------------------------------------------------
# E2: geometry.meshes の path が絶対パスに変換されること
# ---------------------------------------------------------------------------
class TestGenerateTmpYamlAbsolutePaths:
"""E2 系: geometry.meshes の STL パスが絶対パスに変換されること."""
@pytest.fixture(autouse=True)
def _patch_tmp_dir(self, tmp_path, sweep_module):
"""TMP_DIR を tmp_path に差し替える."""
with mock.patch.object(sweep_module, "TMP_DIR", tmp_path):
yield
def test_all_mesh_paths_are_absolute(self, sweep_module):
"""全メッシュの path が絶対パスになっていること.
仕様 E2: 一時 YAML の geometry.meshes の各 path が絶対パスに変換されている
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
meshes = data["geometry"]["meshes"]
assert len(meshes) > 0, "meshes が空のため検証不可"
for mesh in meshes:
p = Path(mesh["path"])
assert p.is_absolute(), (
f"mesh '{mesh.get('id')}' の path が絶対パスでない: {mesh['path']}"
)
def test_absolute_path_resolves_relative_to_template_dir(self, sweep_module):
"""絶対化されたパスがテンプレート YAML の親ディレクトリ(configs/)基準で解決されること.
仕様 E2: template_base = TEMPLATE_YAML.parent(configs/)を基準に resolve
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
meshes = data["geometry"]["meshes"]
template_base = _TEMPLATE_YAML.parent # configs/
for mesh in meshes:
p = Path(mesh["path"])
# 絶対化されたパスは template_base 配下またはそのサブディレクトリにあること
# (relative path が "meshes/xxx.stl" → configs/meshes/xxx.stl の absolute)
try:
p.relative_to(template_base)
except ValueError:
pytest.fail(
f"mesh '{mesh.get('id')}' の絶対パス '{p}' が "
f"template_base '{template_base}' の下にありません(パス解決ロジックの不整合)"
)
def test_no_mesh_path_is_relative(self, sweep_module):
"""一時 YAML のメッシュパスに相対パスが含まれないこと.
仕様 E2: 相対パスが残っていないことを確認
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
meshes = data["geometry"]["meshes"]
for mesh in meshes:
assert Path(mesh["path"]).is_absolute(), (
f"mesh '{mesh.get('id')}' の path が相対パス: {mesh['path']}"
)
# ---------------------------------------------------------------------------
# E3: 一時 YAML が BOM なし UTF-8 で書かれること
# ---------------------------------------------------------------------------
class TestGenerateTmpYamlEncoding:
"""E3 系: 一時 YAML のエンコーディング仕様."""
@pytest.fixture(autouse=True)
def _patch_tmp_dir(self, tmp_path, sweep_module):
"""TMP_DIR を tmp_path に差し替える."""
with mock.patch.object(sweep_module, "TMP_DIR", tmp_path):
yield
def test_output_file_has_no_bom(self, sweep_module):
"""一時 YAML ファイルが UTF-8 BOM(EF BB BF)で始まっていないこと.
仕様 E3: BOM なし UTF-8(Mitsuba YAML パーサは BOM 付きで失敗するため)
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
raw = tmp_yaml.read_bytes()
# UTF-8 BOM は 0xEF 0xBB 0xBF
bom = b"\xef\xbb\xbf"
assert not raw.startswith(bom), (
"一時 YAML が UTF-8 BOM で始まっています.BOM なし UTF-8 で書き込んでください."
)
def test_output_file_is_valid_utf8(self, sweep_module):
"""一時 YAML ファイルが UTF-8 として正常にデコードできること.
仕様 E3: UTF-8 エンコーディングで書かれていること
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
raw = tmp_yaml.read_bytes()
# UnicodeDecodeError が発生しなければ OK
decoded = raw.decode("utf-8")
assert len(decoded) > 0
def test_output_file_parseable_as_yaml(self, sweep_module):
"""一時 YAML ファイルが YAML として正常にパースできること.
仕様 E3: 書き込み後に yaml.safe_load で再読み込みできること(エンコーディング崩れなし)
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
assert isinstance(data, dict)
assert "name" in data
# ---------------------------------------------------------------------------
# E4: name フィールドが "baseline_minitias_blend_{job_name}" 形式になること
# ---------------------------------------------------------------------------
class TestGenerateTmpYamlName:
"""E4 系: 一時 YAML の name フィールド仕様."""
@pytest.fixture(autouse=True)
def _patch_tmp_dir(self, tmp_path, sweep_module):
"""TMP_DIR を tmp_path に差し替える."""
with mock.patch.object(sweep_module, "TMP_DIR", tmp_path):
yield
def test_name_starts_with_baseline_minitias_blend(self, sweep_module):
"""name が "baseline_minitias_blend_" で始まること.
仕様 E4: name フィールドが "baseline_minitias_blend_{job_name}" 形式
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
assert data["name"].startswith("baseline_minitias_blend_")
def test_name_contains_weight_and_alpha(self, sweep_module):
"""name に weight と alpha の値が含まれること.
仕様 E4: job_name = f"w{weight}_a{alpha}" 形式
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
# job_name は "w0.15_a0.3" 相当
name = data["name"]
assert "0.15" in name, f"weight 0.15 が name に含まれない: {name}"
assert "0.3" in name, f"alpha 0.3 が name に含まれない: {name}"
def test_name_format_for_weight_0_05_alpha_0_1(self, sweep_module):
"""weight=0.05, alpha=0.1 のとき name が適切な形式であること.
仕様 E4: スイープグリッドの全組合せで name が正しい形式になる
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.05, 0.1)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
name = data["name"]
assert name.startswith("baseline_minitias_blend_")
assert "0.05" in name
assert "0.1" in name
def test_name_format_for_weight_0_30_alpha_0_6(self, sweep_module):
"""weight=0.30, alpha=0.6 のとき name が適切な形式であること.
仕様 E4: スイープグリッドの最大値でも name が正しい形式になる
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.30, 0.6)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
name = data["name"]
assert name.startswith("baseline_minitias_blend_")
assert "0.3" in name # 0.30 → "0.3" が含まれれば OK
assert "0.6" in name
def test_name_differs_across_different_params(self, sweep_module):
"""異なるパラメータ組み合わせで生成された name が異なること.
仕様 E4: name がジョブ識別子を含むため各ジョブで一意になる
"""
tmp1 = sweep_module._generate_tmp_yaml(0.05, 0.1)
tmp2 = sweep_module._generate_tmp_yaml(0.30, 0.6)
with tmp1.open(encoding="utf-8") as f:
d1 = yaml.safe_load(f)
with tmp2.open(encoding="utf-8") as f:
d2 = yaml.safe_load(f)
assert d1["name"] != d2["name"], "異なるパラメータで同じ name が生成されています"
def test_original_template_name_is_not_used(self, sweep_module):
"""テンプレートの元の name ("baseline_minitias_blend") がそのまま使われないこと.
仕様 E4: name フィールドがジョブごとに更新される
"""
tmp_yaml = sweep_module._generate_tmp_yaml(0.15, 0.3)
with tmp_yaml.open(encoding="utf-8") as f:
data = yaml.safe_load(f)
# テンプレートの元 name は "baseline_minitias_blend"(サフィックスなし)
assert data["name"] != "baseline_minitias_blend", (
"name がテンプレートのまま更新されていません"
)