"""scripts/sweep_blend_bsdf.py の _generate_tmp_yaml 仕様ベーステスト.
仕様出典: 実装サマリー §E
- E1: weight/alpha パラメータが一時 YAML に正しく書き込まれること
- E2: geometry.meshes の STL パスが一時 YAML からの相対パス(POSIX 区切り)に変換されること
(旧仕様は絶対パス化.Windows で生成した YAML を研究室サーバで読めないため 2026-09-13 に
scene/paths.py の relative_mesh_path による YAML 相対パスへ変更)
- 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 フィクスチャ(実ファイル)を使用する(testing.md のモック方針)
"""
from __future__ import annotations
import sys
from pathlib import Path, PurePosixPath, PureWindowsPath
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 が一時 YAML からの相対パス(POSIX 区切り)に変換されること
# ---------------------------------------------------------------------------
class TestGenerateTmpYamlRelativePaths:
"""E2 系: メッシュパスは一時 YAML のディレクトリからの相対パス(POSIX 区切り)で書かれ,
YAML の場所から解決するとテンプレートと同じ実在のメッシュを指すこと.
旧仕様(絶対パス化)は Windows で生成した YAML(C:\\Users\\...)を研究室サーバで
読めないため廃止した(2026-09-13.scene/paths.py relative_mesh_path).
"""
@pytest.fixture(autouse=True)
def _patch_tmp_dir(self, tmp_path, sweep_module):
"""TMP_DIR を tmp_path に差し替える."""
if tmp_path.resolve().drive.upper() != _ROOT.drive.upper():
pytest.skip("tmp_path とリポジトリが別ドライブのため相対パスで表せない環境")
with mock.patch.object(sweep_module, "TMP_DIR", tmp_path):
yield
@staticmethod
def _generate(sweep_module) -> tuple[Path, list[dict]]:
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 が空のため検証不可"
return tmp_yaml, meshes
def test_no_mesh_path_is_absolute(self, sweep_module):
"""全メッシュの path が(POSIX でも Windows でも)絶対パスでないこと.
仕様 E2: 絶対パスだと別マシン(研究室サーバ)で読めない
"""
_, meshes = self._generate(sweep_module)
for mesh in meshes:
p = mesh["path"]
assert not PurePosixPath(p).is_absolute(), f"mesh '{mesh.get('id')}': {p}"
assert not PureWindowsPath(p).is_absolute(), f"mesh '{mesh.get('id')}': {p}"
assert not PureWindowsPath(p).drive, f"mesh '{mesh.get('id')}': {p}"
def test_mesh_paths_use_posix_separator(self, sweep_module):
"""区切り文字は / のみであること(Windows で生成しても Linux で読める)."""
_, meshes = self._generate(sweep_module)
for mesh in meshes:
assert "\\" not in mesh["path"], f"mesh '{mesh.get('id')}': {mesh['path']}"
def test_relative_path_resolves_to_template_mesh(self, sweep_module):
"""YAML の場所から解決すると,テンプレート(configs/)基準の元のメッシュを指すこと.
仕様 E2: template_base = TEMPLATE_YAML.parent(configs/)基準の実体を,
書き出し先 YAML のディレクトリからの相対パスで表す
"""
tmp_yaml, meshes = self._generate(sweep_module)
template = yaml.safe_load(_TEMPLATE_YAML.read_text(encoding="utf-8"))
originals = {m["id"]: m["path"] for m in template["geometry"]["meshes"]}
assert {m["id"] for m in meshes} == set(originals)
for mesh in meshes:
resolved = (tmp_yaml.parent / mesh["path"]).resolve()
expected = (_TEMPLATE_YAML.parent / originals[mesh["id"]]).resolve()
assert resolved == expected, f"mesh '{mesh.get('id')}': {mesh['path']}"
assert resolved.is_file(), f"mesh '{mesh.get('id')}' の実体がありません: {resolved}"
# ---------------------------------------------------------------------------
# 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 がテンプレートのまま更新されていません"
)