"""runtime/llvm_init.py のバージョン付き libLLVM 探索(Ubuntu / Debian / Fedora)の仕様テスト.
仕様出典:
- 実装サマリー 仕様 9: 既存の Windows / POSIX 標準パスに加え,Ubuntu / Debian の
`libLLVM-*.so`(例 `/usr/lib/x86_64-linux-gnu/libLLVM-15.so`)と `/usr/lib/llvm-*/lib/`
(Fedora 形式も)を探す.新しい版を優先する.Windows の既存検出を壊さない.
- docs/02_ENV/ENV_02_環境構築手順.md「研究室サーバ編 / 既知事項」(LLVM)
テスト方針:
- 実ファイルシステムには依存しない.モジュールが参照する `os` と `glob` を
モジュール内だけ差し替えて疑似的な POSIX / Windows 環境を作る
(グローバルな os.name は変えない=pathlib 等に影響させない)
- glob の `*` はパス区切りをまたがない.疑似 glob もその意味論を守る
"""
from __future__ import annotations
import fnmatch
import os
import types
from pathlib import Path
import pytest
import sm_tias_lightsim.runtime.llvm_init as llvm_init
_WINDOWS_STANDARD = [
r"C:\Program Files\LLVM\bin\LLVM-C.dll",
r"C:\Program Files (x86)\LLVM\bin\LLVM-C.dll",
r"C:\ProgramData\chocolatey\lib\llvm\tools\LLVM\bin\LLVM-C.dll",
r"C:\msys64\mingw64\bin\LLVM-C.dll",
r"C:\msys64\clang64\bin\LLVM-C.dll",
]
def _fake_glob(files: set[str]):
def glob(pattern: str) -> list[str]:
pat = pattern.split("/")
hits = []
for f in files:
parts = f.split("/")
if len(parts) == len(pat) and all(
fnmatch.fnmatchcase(p, q) for p, q in zip(parts, pat, strict=True)
):
hits.append(f)
return hits
return glob
@pytest.fixture
def fake_fs(monkeypatch):
"""疑似ファイルシステムを組み立てる関数を返す.戻り値の environ は疑似 os.environ."""
def install(files: set[str], *, os_name: str = "posix", environ: dict | None = None):
env = {} if environ is None else environ
monkeypatch.setattr(llvm_init, "os", types.SimpleNamespace(name=os_name, environ=env))
if os_name == "nt":
def _no_glob(pattern): # Windows 分岐では glob を使わない
raise AssertionError(f"Windows で glob が呼ばれた: {pattern}")
monkeypatch.setattr(llvm_init, "glob", types.SimpleNamespace(glob=_no_glob))
else:
monkeypatch.setattr(llvm_init, "glob", types.SimpleNamespace(glob=_fake_glob(files)))
monkeypatch.setattr(Path, "is_file", lambda self: self.as_posix() in files)
return env
return install
# ---------------------------------------------------------------------------
# Ubuntu / Debian
# ---------------------------------------------------------------------------
def test_ubuntu_versioned_lib_is_found(fake_fs):
"""Newton(Ubuntu 22.04)の `/usr/lib/x86_64-linux-gnu/libLLVM-15.so` を検出すること."""
env = fake_fs({"/usr/lib/x86_64-linux-gnu/libLLVM-15.so"})
result = llvm_init.ensure_drjit_libllvm_path()
assert result == "/usr/lib/x86_64-linux-gnu/libLLVM-15.so"
assert env["DRJIT_LIBLLVM_PATH"] == result
@pytest.mark.parametrize(
"path",
[
"/usr/lib/x86_64-linux-gnu/libLLVM-15.so.1",
"/usr/lib/x86_64-linux-gnu/libLLVM.so.18.1",
"/usr/lib/aarch64-linux-gnu/libLLVM-14.so",
],
)
def test_ubuntu_variants_are_found(fake_fs, path):
fake_fs({path})
assert llvm_init.ensure_drjit_libllvm_path() == path
def test_dev_symlink_preferred_over_soname_of_same_version(fake_fs):
fake_fs(
{
"/usr/lib/x86_64-linux-gnu/libLLVM-15.so.1",
"/usr/lib/x86_64-linux-gnu/libLLVM-15.so",
}
)
assert llvm_init.ensure_drjit_libllvm_path() == "/usr/lib/x86_64-linux-gnu/libLLVM-15.so"
def test_newest_version_is_preferred(fake_fs):
"""複数の版があるときは新しい版を優先すること(数値比較.9 < 11 < 15)."""
fake_fs(
{
"/usr/lib/x86_64-linux-gnu/libLLVM-9.so",
"/usr/lib/x86_64-linux-gnu/libLLVM-15.so",
"/usr/lib/x86_64-linux-gnu/libLLVM-11.so",
}
)
assert llvm_init.ensure_drjit_libllvm_path() == "/usr/lib/x86_64-linux-gnu/libLLVM-15.so"
def test_newest_version_is_preferred_across_directories(fake_fs):
fake_fs(
{
"/usr/lib/llvm-14/lib/libLLVM-14.so",
"/usr/lib/x86_64-linux-gnu/libLLVM-17.so.1",
}
)
assert llvm_init.ensure_drjit_libllvm_path() == "/usr/lib/x86_64-linux-gnu/libLLVM-17.so.1"
def test_libllvm_c_versions_sorted_numerically(fake_fs):
"""既存の `/usr/lib/llvm-*/lib/libLLVM-C.so` も新しい版が先(辞書順の 9 > 15 にしない)."""
fake_fs({"/usr/lib/llvm-9/lib/libLLVM-C.so", "/usr/lib/llvm-15/lib/libLLVM-C.so"})
assert llvm_init.ensure_drjit_libllvm_path() == "/usr/lib/llvm-15/lib/libLLVM-C.so"
# ---------------------------------------------------------------------------
# /usr/lib/llvm-*/lib/(Fedora 形式を含む)と /usr/lib64
# ---------------------------------------------------------------------------
@pytest.mark.parametrize(
"path",
[
"/usr/lib/llvm-17/lib/libLLVM.so",
"/usr/lib/llvm-16/lib/libLLVM-16.so",
"/usr/lib/llvm-18/lib/libLLVM.so.18.1",
"/usr/lib/llvm-18/lib/libLLVM-18.so.1",
"/usr/lib64/libLLVM-17.so",
"/usr/lib64/libLLVM.so",
"/usr/lib64/libLLVM.so.18.1",
],
)
def test_llvm_dir_and_fedora_forms_are_found(fake_fs, path):
fake_fs({path})
assert llvm_init.ensure_drjit_libllvm_path() == path
# ---------------------------------------------------------------------------
# 既存挙動の維持・候補リストの性質
# ---------------------------------------------------------------------------
def test_existing_libllvm_c_is_still_found(fake_fs):
fake_fs({"/usr/lib/llvm-15/lib/libLLVM-C.so"})
assert llvm_init.ensure_drjit_libllvm_path() == "/usr/lib/llvm-15/lib/libLLVM-C.so"
def test_existing_fixed_posix_paths_remain_candidates(fake_fs):
fake_fs(set())
candidates = llvm_init._iter_default_candidates()
for path in [
"/usr/local/lib/libLLVM-C.so",
"/opt/homebrew/opt/llvm/lib/libLLVM-C.dylib",
"/usr/local/opt/llvm/lib/libLLVM-C.dylib",
]:
assert path in candidates
def test_all_forms_are_candidates_and_newest_first(fake_fs):
"""全形式を候補に含み,同一バージョンが並んでも .so(開発用リンク)から試すこと.
※ `libLLVM-*.so` パターンは `libLLVM-C.so` にも一致するため,libLLVM-C.so が候補に
2 回現れうる(is_file の確認が 1 回増えるだけで結果は変わらない).仕様上の要件では
ないので重複の有無は検証しない.
"""
fake_fs(
{
"/usr/lib/llvm-15/lib/libLLVM-C.so",
"/usr/lib/llvm-15/lib/libLLVM.so",
"/usr/lib/llvm-15/lib/libLLVM-15.so",
"/usr/lib/llvm-15/lib/libLLVM-15.so.1",
"/usr/lib/llvm-15/lib/libLLVM.so.15",
"/usr/lib/x86_64-linux-gnu/libLLVM-15.so",
"/usr/lib/x86_64-linux-gnu/libLLVM-15.so.1",
"/usr/lib/x86_64-linux-gnu/libLLVM.so.15",
"/usr/lib64/libLLVM-15.so",
}
)
candidates = llvm_init._iter_default_candidates()
for path in [
"/usr/lib/llvm-15/lib/libLLVM.so",
"/usr/lib/llvm-15/lib/libLLVM-15.so",
"/usr/lib/llvm-15/lib/libLLVM.so.15",
"/usr/lib/x86_64-linux-gnu/libLLVM-15.so",
"/usr/lib/x86_64-linux-gnu/libLLVM-15.so.1",
"/usr/lib/x86_64-linux-gnu/libLLVM.so.15",
"/usr/lib64/libLLVM-15.so",
]:
assert path in candidates
so_1 = candidates.index("/usr/lib/x86_64-linux-gnu/libLLVM-15.so.1")
assert candidates.index("/usr/lib/x86_64-linux-gnu/libLLVM-15.so") < so_1
def test_unrelated_libraries_are_not_candidates(fake_fs, caplog):
"""libLLVM 以外(静的ライブラリ・別名のライブラリ)は拾わないこと."""
env = fake_fs(
{
"/usr/lib/x86_64-linux-gnu/libLLVM-15.a",
"/usr/lib/x86_64-linux-gnu/libLLVMSupport.so",
"/usr/lib/x86_64-linux-gnu/libclang-15.so",
"/usr/lib/x86_64-linux-gnu/sub/libLLVM-15.so", # glob の * は / をまたがない
}
)
assert llvm_init.ensure_drjit_libllvm_path() is None
assert "DRJIT_LIBLLVM_PATH" not in env
def test_existing_env_is_respected_on_posix(fake_fs):
env = fake_fs(
{"/usr/lib/x86_64-linux-gnu/libLLVM-15.so"},
environ={"DRJIT_LIBLLVM_PATH": "/custom/libLLVM.so"},
)
assert llvm_init.ensure_drjit_libllvm_path() == "/custom/libLLVM.so"
assert env["DRJIT_LIBLLVM_PATH"] == "/custom/libLLVM.so"
# ---------------------------------------------------------------------------
# Windows の既存検出を壊していない
# ---------------------------------------------------------------------------
def test_windows_candidates_unchanged(fake_fs):
"""Windows 分岐の候補は従来の 5 パスのまま(POSIX の glob を呼ばない)."""
fake_fs(set(), os_name="nt")
assert llvm_init._iter_default_candidates() == _WINDOWS_STANDARD
def test_windows_llvm_dir_still_first(fake_fs):
fake_fs(set(), os_name="nt", environ={"LLVM_DIR": r"D:\llvm"})
candidates = llvm_init._iter_default_candidates()
assert candidates[0] == str(Path(r"D:\llvm") / "bin" / "LLVM-C.dll")
assert candidates[1:] == _WINDOWS_STANDARD
@pytest.mark.skipif(os.name != "nt", reason="実 Windows 環境での回帰確認")
def test_real_windows_candidates_have_no_posix_paths(monkeypatch):
monkeypatch.delenv("LLVM_DIR", raising=False)
assert llvm_init._iter_default_candidates() == _WINDOWS_STANDARD