"""スイープ生成スクリプトが一時 YAML に書くメッシュパスの仕様ベーステスト.
仕様出典:
- 実装サマリー 仕様 10: スイープ生成スクリプトは _tmp YAML のメッシュパスを
YAML ファイルからの相対パス(POSIX 区切り)で書く(scene/paths.py relative_mesh_path).
以前は絶対パスだった
- docs/02_ENV/ENV_04_開発コマンド.md「研究室サーバでの実行」注意
(Windows 絶対パスの YAML はサーバで読めない.push 時に警告が出る)
対象(9 本): sanity_led_groups / sweep_max_depth / sweep_led_distribution / sweep_reflectance /
sweep_blend_bsdf / sweep_bsdf_grid_fix / sweep_sphere_radius / sweep_cube_size / sweep_offaxis_led
テスト方針:
- sm-render は呼ばない.YAML 生成関数だけを呼び,出力先は tmp_path に差し替える
- 「YAML の場所から解決すると元のメッシュ(テンプレートの実在メッシュ・生成メッシュ)を指す」
「どの OS でも相対パスとして読める(区切りは /,ドライブ無し)」
「push の Windows 絶対パス警告に掛からない」の 3 点を確かめる
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path, PurePosixPath, PureWindowsPath
from types import ModuleType
from unittest import mock
import pytest
import yaml
from sm_tias_lightsim.remote.files import PushEntry, find_yaml_with_windows_paths
_ROOT = Path(__file__).resolve().parents[2]
_SCRIPTS = _ROOT / "scripts"
def _load(name: str) -> ModuleType:
"""scripts/<name>.py を import する(既に読み込み済みならそれを使う)."""
path = (_SCRIPTS / f"{name}.py").resolve()
cached = sys.modules.get(name)
if cached is not None and Path(getattr(cached, "__file__", "")).resolve() == path:
return cached
spec = importlib.util.spec_from_file_location(name, path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module
@pytest.fixture(autouse=True)
def _same_drive(tmp_path):
if tmp_path.resolve().drive.upper() != _ROOT.drive.upper():
pytest.skip("tmp_path とリポジトリが別ドライブのため相対パスで表せない環境")
def _template_targets(template: Path) -> dict[str, Path]:
"""テンプレート YAML のメッシュ id → 実体(テンプレートのディレクトリ基準で解決)."""
data = yaml.safe_load(template.read_text(encoding="utf-8"))
return {m["id"]: (template.parent / m["path"]).resolve() for m in data["geometry"]["meshes"]}
def _assert_mesh_paths(yaml_path: Path, expected: dict[str, Path]) -> None:
data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
meshes = data["geometry"]["meshes"]
assert {m["id"] for m in meshes} == set(expected)
for mesh in meshes:
path = mesh["path"]
assert isinstance(path, str)
assert "\\" not in path, f"{mesh['id']}: POSIX 区切りでない: {path}"
assert not PurePosixPath(path).is_absolute(), f"{mesh['id']}: 絶対パス: {path}"
assert not PureWindowsPath(path).is_absolute(), f"{mesh['id']}: 絶対パス: {path}"
assert not PureWindowsPath(path).drive, f"{mesh['id']}: ドライブ付き: {path}"
assert (yaml_path.parent / path).resolve() == expected[mesh["id"]], mesh["id"]
entry = PushEntry(path=yaml_path, arcname=f"configs/_tmp_x/{yaml_path.name}", size=0)
assert find_yaml_with_windows_paths([entry]) == []
def _require_existing(targets: dict[str, Path]) -> None:
missing = [str(p) for p in targets.values() if not p.is_file()]
if missing:
pytest.skip(f"テンプレートのメッシュが実在しない環境: {missing}")
# ---------------------------------------------------------------------------
# テンプレートのメッシュをそのまま参照する 5 本(_generate_tmp_yaml)
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
("script", "args"),
[
("sanity_led_groups", ("bottom",)),
("sweep_max_depth", (16,)),
("sweep_led_distribution", (90,)),
("sweep_reflectance", (0.85,)),
("sweep_blend_bsdf", (0.15, 0.3)),
],
)
def test_generate_tmp_yaml_writes_yaml_relative_paths(tmp_path, script, args):
module = _load(script)
tmp_dir = tmp_path / "configs" / f"_tmp_{script}"
with mock.patch.object(module, "TMP_DIR", tmp_dir):
yaml_path = module._generate_tmp_yaml(*args)
assert yaml_path.parent == tmp_dir
expected = _template_targets(module.TEMPLATE_YAML)
_require_existing(expected)
_assert_mesh_paths(yaml_path, expected)
def test_sweep_bsdf_grid_fix_writes_yaml_relative_paths(tmp_path):
module = _load("sweep_bsdf_grid_fix")
if not module.TEMPLATE_YAML.is_file():
pytest.skip(f"テンプレートがありません: {module.TEMPLATE_YAML}")
expected = _template_targets(module.TEMPLATE_YAML)
_require_existing(expected)
dest = tmp_path / "configs" / "_tmp_bsdf_grid" / "r0.85_w0.05_a0.1" / "scene.yaml"
module.build_scene_yaml(0.85, 0.05, 0.1, dest)
_assert_mesh_paths(dest, expected)
# ---------------------------------------------------------------------------
# 生成メッシュ(筐体・バッフル)を参照する 3 本(build_scene_yaml)
# ---------------------------------------------------------------------------
@pytest.mark.parametrize("with_baffle", [True, False])
def test_sweep_sphere_radius_writes_yaml_relative_paths(tmp_path, with_baffle):
module = _load("sweep_sphere_radius")
tmp_dir = tmp_path / "configs" / "_tmp_sphere_radius"
mesh = tmp_dir / "meshes" / "球_r100.stl"
baffle = tmp_dir / "meshes" / "バッフル_r100.stl"
dest = tmp_dir / "board_baffle" / "scene_r100.yaml"
module.build_scene_yaml(
100.0, mesh, dest, "board_baffle", "similar_radial", baffle if with_baffle else None
)
expected = _template_targets(module.TEMPLATE_YAML)
expected["sphere_housing"] = mesh.resolve()
if with_baffle:
expected["baffle"] = baffle.resolve()
_assert_mesh_paths(dest, expected)
paths = {
m["id"]: m["path"]
for m in yaml.safe_load(dest.read_text(encoding="utf-8"))["geometry"]["meshes"]
}
assert paths["sphere_housing"] == "../meshes/球_r100.stl"
if with_baffle:
assert paths["baffle"] == "../meshes/バッフル_r100.stl"
def test_sweep_cube_size_writes_yaml_relative_paths(tmp_path):
module = _load("sweep_cube_size")
tmp_dir = tmp_path / "configs" / "_tmp_cube_size"
mesh = tmp_dir / "meshes" / "立方体_L120.stl"
baffle = tmp_dir / "meshes" / "バッフル_L120.stl"
dest = tmp_dir / "scene_L120.yaml"
module.build_scene_yaml(120.0, mesh, dest, baffle)
expected = _template_targets(module.TEMPLATE_YAML)
expected.pop("sphere_housing")
expected["cube_housing"] = mesh.resolve()
expected["baffle"] = baffle.resolve()
_assert_mesh_paths(dest, expected)
@pytest.mark.parametrize(("shape", "size"), [("cube", 120.0), ("sphere", 100.0)])
def test_sweep_offaxis_led_writes_yaml_relative_paths(tmp_path, shape, size):
module = _load("sweep_offaxis_led")
tmp_dir = tmp_path / "configs" / "_tmp_offaxis"
name = module.job_name(shape, size)
mesh = tmp_dir / "meshes" / f"{name}.stl"
baffle = tmp_dir / "meshes" / f"{name}_baffle.stl"
dest = tmp_dir / f"scene_{name}.yaml"
module.build_scene_yaml(shape, size, mesh, baffle, module.led_geometry(shape, size), dest)
expected = _template_targets(module.TEMPLATE_YAML)
expected.pop("sphere_housing")
expected["cube_housing" if shape == "cube" else "sphere_housing"] = mesh.resolve()
expected["baffle"] = baffle.resolve()
_assert_mesh_paths(dest, expected)