"""scene/paths.py(relative_mesh_path)の仕様ベーステスト.
仕様出典:
- 実装サマリー 仕様 10: スイープ生成スクリプトは _tmp YAML のメッシュパスを
YAML ファイルからの相対パス(POSIX 区切り)で書く
- docs/02_ENV/ENV_04_開発コマンド.md「研究室サーバでの実行」注意(Windows 絶対パスは
サーバで読めない.push 時に警告が出る)
テスト方針:
- 相対パスは「YAML のディレクトリから解決すると元のメッシュを指す」ことで検証する
- 相対化できない場合(Windows で別ドライブ)は,警告を出して絶対パスを返す
(docstring に明記された従来挙動へのフォールバック).この扱いをテストで明示する
"""
from __future__ import annotations
import logging
import os
import string
from pathlib import Path, PurePosixPath, PureWindowsPath
import pytest
from sm_tias_lightsim.remote.files import PushEntry, find_yaml_with_windows_paths
from sm_tias_lightsim.scene import paths
from sm_tias_lightsim.scene.paths import relative_mesh_path
_ROOT = Path(__file__).resolve().parents[2]
def _assert_portable(rel: str) -> None:
"""どの OS でも相対パスとして解釈される文字列であること."""
assert "\\" not in rel, f"POSIX 区切りでない: {rel}"
assert not PurePosixPath(rel).is_absolute(), rel
assert not PureWindowsPath(rel).is_absolute(), rel
assert not PureWindowsPath(rel).drive, rel
@pytest.mark.parametrize(
("target", "yaml_dir", "expected"),
[
("configs/scene.stl", "configs", "scene.stl"),
("configs/meshes/前側.stl", "configs", "meshes/前側.stl"),
("configs/meshes/前側.stl", "configs/_tmp_offaxis", "../meshes/前側.stl"),
(
"configs/_tmp_offaxis/meshes/立方体_L120.stl",
"configs/_tmp_offaxis",
"meshes/立方体_L120.stl",
),
("configs/meshes/a.stl", "configs/_tmp_x/deep/er", "../../../meshes/a.stl"),
("other/b.stl", "configs/_tmp_x", "../../other/b.stl"),
],
)
def test_relative_to_yaml_dir(tmp_path, target, yaml_dir, expected):
"""YAML のディレクトリからの相対パスを POSIX 区切りで返すこと."""
rel = relative_mesh_path(tmp_path / target, tmp_path / yaml_dir)
assert rel == expected
_assert_portable(rel)
def test_resolves_back_to_target(tmp_path):
"""YAML のディレクトリ基準で解決すると実在のメッシュを指すこと.
YAML のディレクトリは実在させておく(実運用でも YAML を書き出す場所なので必ずある).
POSIX ではパス中の `..` をカーネルがたどるため,手前のディレクトリが無いと
`_tmp_blend/../meshes/...` へのアクセスが ENOENT になる(Windows は `..` を文字列上で
先に畳むので通ってしまい,差が出る).
"""
mesh = tmp_path / "configs" / "meshes" / "照明ムラ補正ボード.stl"
mesh.parent.mkdir(parents=True)
mesh.write_text("solid", encoding="utf-8")
yaml_dir = tmp_path / "configs" / "_tmp_blend"
yaml_dir.mkdir(parents=True)
rel = relative_mesh_path(mesh, yaml_dir)
assert (yaml_dir / rel).resolve() == mesh.resolve()
assert (yaml_dir / rel).is_file()
def test_yaml_dir_need_not_exist(tmp_path):
"""書き出し前(YAML ディレクトリ未作成)でも計算できること."""
rel = relative_mesh_path(tmp_path / "meshes" / "a.stl", tmp_path / "not" / "yet")
assert rel == "../../meshes/a.stl"
def test_relative_target_is_resolved_from_cwd(tmp_path, monkeypatch):
"""target が相対パスなら CWD 基準で解釈すること."""
monkeypatch.chdir(tmp_path)
rel = relative_mesh_path(Path("meshes") / "a.stl", tmp_path / "cfg")
assert rel == "../meshes/a.stl"
def test_repository_layout_gives_short_relative_path():
"""実リポジトリの configs/_tmp_* → configs/meshes は `../meshes/...` になること
(サーバの任意の場所に置いたリポジトリでもそのまま解決できる)."""
rel = relative_mesh_path(
_ROOT / "configs" / "meshes" / "前側.stl", _ROOT / "configs" / "_tmp_offaxis"
)
assert rel == "../meshes/前側.stl"
def test_portable_path_is_not_flagged_by_push_warning(tmp_path):
"""相対パスで書いた YAML は push の Windows 絶対パス警告に引っかからないこと."""
rel = relative_mesh_path(tmp_path / "configs" / "meshes" / "a.stl", tmp_path / "configs" / "_t")
yaml_path = tmp_path / "configs" / "_t" / "scene.yaml"
yaml_path.parent.mkdir(parents=True)
yaml_path.write_text(f"geometry:\n meshes:\n - id: a\n path: {rel}\n", encoding="utf-8")
entry = PushEntry(path=yaml_path, arcname="configs/_t/scene.yaml", size=1)
assert find_yaml_with_windows_paths([entry]) == []
# ---------------------------------------------------------------------------
# 相対化できない場合(Windows の別ドライブ)の扱い
# ---------------------------------------------------------------------------
def test_unrelatable_path_falls_back_to_absolute_with_warning(tmp_path, monkeypatch, caplog):
"""os.path.relpath が ValueError(別ドライブ)のときは例外にせず,警告を出して
解決済みの絶対パスを返すこと(従来挙動へのフォールバック)."""
def _raise(*_args, **_kwargs):
raise ValueError("path is on mount 'D:', start on mount 'C:'")
monkeypatch.setattr(paths.os.path, "relpath", _raise)
target = tmp_path / "meshes" / "a.stl"
with caplog.at_level(logging.WARNING, logger=paths.__name__):
result = relative_mesh_path(target, tmp_path / "cfg")
assert result == str(target.resolve())
assert Path(result).is_absolute()
assert any(r.levelno == logging.WARNING for r in caplog.records)
def _other_drive_letter() -> str:
current = Path.cwd().drive.rstrip(":").upper()
for letter in reversed(string.ascii_uppercase):
if letter != current and letter != Path(os.environ.get("TEMP", "C:")).drive[:1].upper():
return letter
raise RuntimeError("別ドライブ文字が見つかりません")
@pytest.mark.skipif(os.name != "nt", reason="ドライブ文字は Windows のみ")
def test_windows_other_drive_returns_absolute_and_push_warns(tmp_path, caplog):
"""Windows で YAML とメッシュのドライブが違う場合: 絶対パスを返し(警告ログ付き),
その YAML を push すると Windows 絶対パスとして警告対象になること(安全網が効く)."""
letter = _other_drive_letter()
target = Path(f"{letter}:\\meshes\\a.stl")
with caplog.at_level(logging.WARNING, logger=paths.__name__):
result = relative_mesh_path(target, tmp_path)
assert PureWindowsPath(result).drive.upper() == f"{letter}:"
assert PureWindowsPath(result).is_absolute()
assert any(r.levelno == logging.WARNING for r in caplog.records)
yaml_path = tmp_path / "scene.yaml"
yaml_path.write_text(f"geometry:\n meshes:\n - id: a\n path: {result}\n", encoding="utf-8")
entry = PushEntry(path=yaml_path, arcname="configs/_tmp_x/scene.yaml", size=1)
assert find_yaml_with_windows_paths([entry]) == ["configs/_tmp_x/scene.yaml"]