"""Studio install-root inference must not crash under hostile filesystem conditions (PermissionError/OSError swallowed; custom root kept when resolve() fails).""" from __future__ import annotations import importlib.util import sys import textwrap from pathlib import Path from unittest import mock import pytest REPO_ROOT = Path(__file__).resolve().parents[1] STORAGE_ROOTS = REPO_ROOT / "studio" / "backend" / "utils" / "paths" / "storage_roots.py" LLAMA_CPP = REPO_ROOT / "studio" / "backend" / "core" / "inference" / "llama_cpp.py" def _load(name: str, path: Path): spec = importlib.util.spec_from_file_location(name, path) assert spec is not None and spec.loader is not None mod = importlib.util.module_from_spec(spec) sys.modules[name] = mod spec.loader.exec_module(mod) return mod def test_infer_studio_home_swallows_permission_error(tmp_path, monkeypatch): candidate = tmp_path / "fake_root" venv = candidate / "unsloth_studio" venv.mkdir(parents = True) monkeypatch.setattr(sys, "prefix", str(venv)) sys.modules.pop("sr_perm", None) mod = _load("sr_perm", STORAGE_ROOTS) with mock.patch.object(Path, "is_file", side_effect = PermissionError("denied")): # Must NOT raise. assert mod._infer_studio_home_from_venv() is None def test_studio_root_does_not_crash_on_permission_error(tmp_path, monkeypatch): """studio_root() falls through to the legacy default on a restricted filesystem.""" candidate = tmp_path / "fake_root" venv = candidate / "unsloth_studio" venv.mkdir(parents = True) monkeypatch.setattr(sys, "prefix", str(venv)) monkeypatch.delenv("UNSLOTH_STUDIO_HOME", raising = False) monkeypatch.delenv("STUDIO_HOME", raising = False) sys.modules.pop("sr_studio_perm", None) mod = _load("sr_studio_perm", STORAGE_ROOTS) with mock.patch.object(Path, "is_file", side_effect = OSError("ebusy")): result = mod.studio_root() assert result == Path.home() / ".unsloth" / "studio" def _method_body(src: str, name: str) -> str: """Whole method body (def to next sibling def at the same indent).""" start = src.index(f"def {name}") indent = " " * (start - src.rfind("\n", 0, start) - 1) nxt = src.find(f"\n{indent}def ", start + 1) return src[start : nxt if nxt != -1 else len(src)] def test_kill_orphan_catches_oserror_from_studio_root(): """Cleanup must not crash when studio_root() raises. _kill_orphaned_servers resolves the install root through the shared _resolved_studio_root_and_is_legacy() classifier, which swallows (ImportError, OSError, ValueError) on the probe.""" src = LLAMA_CPP.read_text() # Cleanup delegates to the shared classifier rather than importing studio_root inline. assert "LlamaCppBackend._resolved_studio_root_and_is_legacy()" in _method_body( src, "_kill_orphaned_servers" ), "_kill_orphaned_servers must resolve the root via _resolved_studio_root_and_is_legacy()" # The shared classifier catches both the resolve() failure and the outer studio_root() probe. classifier = _method_body(src, "_resolved_studio_root_and_is_legacy") assert "studio_root as _sr" in classifier, "classifier must probe studio_root()" assert "except (OSError, ValueError):" in classifier, "inner resolve() probe must be guarded" assert "except (ImportError, OSError, ValueError):" in classifier, ( "_resolved_studio_root_and_is_legacy must catch (ImportError, OSError, ValueError) " "from studio_root()" ) def _exec_search_roots_block( home: Path, studio_root_value: Path, resolve_raises: bool ) -> list[Path]: """Run _find_llama_server_binary's search_roots derivation -- plus the shared _resolved_studio_root_and_is_legacy() classifier it delegates to -- with a controlled studio_root() and resolve(), without importing the heavy module.""" src = LLAMA_CPP.read_text() # Shared root classifier (holds the defensive try/except for studio_root()). # End the slice at the next sibling def/decorator at the same indent rather # than the literal "@staticmethod" string, so a future docstring mentioning a # decorator can't truncate the helper mid-body and break exec(). helper_start = src.index("def _resolved_studio_root_and_is_legacy") indent = " " * (helper_start - src.rfind("\n", 0, helper_start) - 1) nxt_def = src.find(f"\n{indent}def ", helper_start + 1) nxt_dec = src.find(f"\n{indent}@", helper_start + 1) sibling = [idx for idx in (nxt_def, nxt_dec) if idx != -1] helper_end = min(sibling) if sibling else len(src) helper = textwrap.dedent(src[helper_start:helper_end]) # search_roots derivation inside _find_llama_server_binary (delegates to the classifier). block_start = src.index('legacy_llama = Path.home() / ".unsloth" / "llama.cpp"') block_end = src.index("for unsloth_home in search_roots:", block_start) block = textwrap.dedent(" " * 8 + src[block_start:block_end]) fake_module = type(sys)("fake_storage_roots") fake_module.studio_root = lambda: studio_root_value sys.modules["utils.paths.storage_roots"] = fake_module try: original_resolve = Path.resolve def _resolve(self, *a, **k): if resolve_raises: raise OSError("ebusy") return original_resolve(self, *a, **k) with ( mock.patch.object(Path, "home", classmethod(lambda cls: home)), mock.patch.object(Path, "resolve", _resolve), ): ns: dict = {"Path": Path} exec(helper, ns) # noqa: S102 -- defines _resolved_studio_root_and_is_legacy ns["LlamaCppBackend"] = type( "LlamaCppBackend", (), { "_resolved_studio_root_and_is_legacy": staticmethod( ns["_resolved_studio_root_and_is_legacy"] ) }, ) exec(block, ns) # noqa: S102 -- defines search_roots return ns["search_roots"] finally: sys.modules.pop("utils.paths.storage_roots", None) def test_search_roots_keeps_custom_when_resolve_fails(tmp_path): home = tmp_path / "home" home.mkdir() custom = tmp_path / "custom_studio" custom.mkdir() roots = _exec_search_roots_block(home = home, studio_root_value = custom, resolve_raises = True) # On resolve() failure, the inner except falls back to direct equality; # custom != legacy_studio so the custom root must remain in search_roots. assert custom / "llama.cpp" in roots, f"custom root dropped on resolve() failure: {roots}" # custom-mode discovery excludes the legacy tree to match _kill_orphaned_servers. assert ( home / ".unsloth" / "llama.cpp" ) not in roots, f"legacy llama path must not appear in custom-mode search_roots: {roots}" def test_search_roots_default_mode_uses_legacy_only(tmp_path): home = tmp_path / "home" home.mkdir() legacy = home / ".unsloth" / "studio" legacy.mkdir(parents = True) roots = _exec_search_roots_block(home = home, studio_root_value = legacy, resolve_raises = False) # Default mode: only legacy_llama. assert roots == [home / ".unsloth" / "llama.cpp"]