unsloth/tests/test_setup_master_root.py
Daniel Han 50b9deacb0
Trim comments in tests (#13844)
Comment and docstring changes only, in test files, split out of #12968 so the test-only part can merge first.
2026-10-15 05:30:27 -07:00

1340 lines
55 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-only
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
"""Setup must place the runtimes at UNSLOTH_HOME, not under the studio home the CLI exports."""
from __future__ import annotations
import os
import pathlib
import re
import shutil
import subprocess
from pathlib import Path
import pytest
# tests/conftest.py puts tests/_shared on sys.path; see unsloth_pwsh_runner for why a bare
# pwsh subprocess is not enough under xdist.
from unsloth_pwsh_runner import PWSH, run_pwsh
REPO_ROOT = Path(__file__).resolve().parents[1]
SETUP_SH = REPO_ROOT / "studio" / "setup.sh"
SETUP_PS1 = REPO_ROOT / "studio" / "setup.ps1"
# Needs POSIX bash: Git Bash on Windows runs these against Windows paths and UTF-16.
NEEDS_POSIX_BASH = pytest.mark.skipif(
shutil.which("bash") is None or os.name == "nt",
reason = "runs studio/setup.sh blocks: needs bash on a POSIX filesystem",
)
def _ps_quote(value: str) -> str:
"""A PowerShell single-quoted literal: only the quote itself needs escaping."""
return "'" + value.replace("'", "''") + "'"
def _slice(src: str, start: str, end: str) -> str:
begin = src.index(start)
return src[begin : src.index(end, begin)]
def _runtime_parent(env: dict[str, str]) -> tuple[str, str]:
"""Runs the shipped setup.sh blocks rather than matching text, so reordering them still fails."""
src = SETUP_SH.read_text(encoding = "utf-8")
master = _slice(src, "# Stripped before anything else", "# Directory-local evidence")
node = _slice(src, "# Mirror the llama.cpp UNSLOTH_HOME derivation", "NODE_DIR=")
llama = _slice(src, 'if [ -n "$STAGE_ROOT" ]; then\n UNSLOTH_HOME=', "LLAMA_CPP_DIR=")
script = "\n".join(
(
"set -u",
master,
node,
llama,
'printf "%s\\n%s\\n" "$_NODE_PARENT" "$UNSLOTH_HOME"',
)
)
completed = subprocess.run(
["bash", "-c", script],
env = env,
capture_output = True,
text = True,
timeout = 60,
)
assert completed.returncode == 0, completed.stderr
node_parent, llama_parent = completed.stdout.splitlines()
return node_parent, llama_parent
def _env(home: Path, **overrides: str) -> dict[str, str]:
env = {
"HOME": str(home),
"PATH": "/usr/bin:/bin",
"STAGE_ROOT": "",
"RUNTIME_ROOT": "",
"STUDIO_HOME": str(home / ".unsloth" / "studio"),
"_STUDIO_HOME_IS_CUSTOM": "false",
}
env.update(overrides)
return env
def _master_root_answer(tmp_path: Path, environment: str) -> str:
"""Runs the shipped _MasterRoot from uninstall.ps1 so a rewrite that keeps only the words fails."""
script = tmp_path / "probe.ps1"
script.write_text(
f"""$txt = Get-Content -Raw "{UNINSTALL_PS1}"
foreach ($n in @("_ExpandTilde", "_MasterRoot")) {{
$m = [regex]::Match($txt, "(?ms)^ function $n \\{{.*?^ \\}}")
if (-not $m.Success) {{ Write-Output "EXTRACT-FAILED:$n"; exit 1 }}
Invoke-Expression $m.Value
}}
{environment}
""",
encoding = "utf-8",
)
out = run_pwsh(
[PWSH, "-NoProfile", "-File", str(script)],
capture_output = True,
text = True,
check = True,
).stdout.strip()
assert "EXTRACT-FAILED" not in out, out
return out
@NEEDS_POSIX_BASH
def test_a_master_root_puts_the_runtimes_beside_studio(tmp_path):
root = tmp_path / "portable"
(root / "studio").mkdir(parents = True)
env = _env(
tmp_path / "home",
UNSLOTH_HOME = str(root),
# What unsloth_cli/commands/studio.py exports before it runs setup.
STUDIO_HOME = str(root / "studio"),
_STUDIO_HOME_IS_CUSTOM = "true",
)
node_parent, llama_parent = _runtime_parent(env)
assert Path(node_parent) == root
assert Path(llama_parent) == root
@NEEDS_POSIX_BASH
def test_no_master_root_keeps_the_studio_home_layout(tmp_path):
"""The pre-existing custom-root behaviour, which the new branch must not disturb."""
studio = tmp_path / "elsewhere" / "studio"
studio.mkdir(parents = True)
env = _env(
tmp_path / "home",
STUDIO_HOME = str(studio),
_STUDIO_HOME_IS_CUSTOM = "true",
)
node_parent, llama_parent = _runtime_parent(env)
assert Path(node_parent) == studio
assert Path(llama_parent) == studio
@NEEDS_POSIX_BASH
def test_a_default_install_still_uses_the_legacy_root(tmp_path):
home = tmp_path / "home"
(home / ".unsloth" / "studio").mkdir(parents = True)
node_parent, llama_parent = _runtime_parent(_env(home))
assert Path(node_parent) == home / ".unsloth"
assert Path(llama_parent) == home / ".unsloth"
@NEEDS_POSIX_BASH
def test_a_staging_root_still_outranks_the_master_root(tmp_path):
stage = tmp_path / "stage"
stage.mkdir()
env = _env(
tmp_path / "home",
UNSLOTH_HOME = str(tmp_path / "portable"),
STAGE_ROOT = str(stage),
RUNTIME_ROOT = str(stage),
)
node_parent, llama_parent = _runtime_parent(env)
assert Path(node_parent) == stage
assert Path(llama_parent) == stage
def test_setup_ps1_derives_both_runtimes_from_the_master_root():
"""No PowerShell on the Linux runners, so the Windows half is held structurally: both
derivations must go through the one helper, and it must read UNSLOTH_HOME."""
src = SETUP_PS1.read_text(encoding = "utf-8")
assert "function Get-MasterRootOverride" in src
helper = _slice(src, "function Get-MasterRootOverride", "function Get-ManagedLlamaCppDir")
assert "$env:UNSLOTH_HOME" in helper
llama = _slice(src, "function Get-ManagedLlamaCppDir", "\n# Failure reason when the managed")
assert "Get-MasterRootOverride" in llama
# The master root must lose to an explicit staging root, as in setup.sh.
assert llama.index("$StagingRoot") < llama.index("Get-MasterRootOverride")
node = _slice(src, " $_masterRoot = Get-MasterRootOverride", " $NodeDir = Join-Path")
assert "$NodeParent = $_masterRoot" in node
assert node.index("$StageRoot") < node.index("$NodeParent = $_masterRoot")
@NEEDS_POSIX_BASH
def test_a_padded_master_root_names_the_same_directory(tmp_path):
"""storage_roots.unsloth_home() and the CLI both .strip(), so setup has to as well: an
unstripped value installs under a directory whose name carries the whitespace."""
root = tmp_path / "portable"
(root / "studio").mkdir(parents = True)
env = _env(
tmp_path / "home",
UNSLOTH_HOME = f" {root} ",
STUDIO_HOME = str(root / "studio"),
_STUDIO_HOME_IS_CUSTOM = "true",
)
node_parent, llama_parent = _runtime_parent(env)
assert Path(node_parent) == root
assert Path(llama_parent) == root
@NEEDS_POSIX_BASH
def test_a_blank_master_root_counts_as_unset(tmp_path):
home = tmp_path / "home"
(home / ".unsloth" / "studio").mkdir(parents = True)
node_parent, llama_parent = _runtime_parent(_env(home, UNSLOTH_HOME = " "))
assert Path(node_parent) == home / ".unsloth"
assert Path(llama_parent) == home / ".unsloth"
BUILD_WHISPER = REPO_ROOT / "scripts" / "build_whisper_cpp.sh"
def _whisper_root_block() -> str:
"""The shipped root selection, from the normalizer down to the INSTALL_DIR it decides."""
src = BUILD_WHISPER.read_text(encoding = "utf-8")
return _slice(src, "_root_value() {", "STUDIO_OWNED_MARKER=")
@pytest.fixture
def whisper_path(tmp_path_factory):
"""Stubs git and cmake on PATH so the shipped whisper block's command -v preflight passes."""
stub_bin = tmp_path_factory.mktemp("stubbin")
for tool in ("git", "cmake"):
path = stub_bin / tool
path.write_text("#!/bin/sh\nexit 0\n", encoding = "utf-8")
path.chmod(0o755)
return f"{stub_bin}:/usr/bin:/bin"
@NEEDS_POSIX_BASH
def test_the_whisper_builder_installs_under_the_master_root(tmp_path, whisper_path):
"""setup.sh runs this with UNSLOTH_STUDIO_HOME=<root>/studio still inherited, so a builder
that preferred it would install a level below _managed_whisper_cpp_dir()."""
root = tmp_path / "portable"
src = BUILD_WHISPER.read_text(encoding = "utf-8")
block = _whisper_root_block()
script = "\n".join(("set -eu", block, 'printf "%s\\n" "$INSTALL_DIR"'))
completed = subprocess.run(
["bash", "-c", script],
env = {
"HOME": str(tmp_path / "home"),
"PATH": whisper_path,
"UNSLOTH_HOME": str(root),
"UNSLOTH_STUDIO_HOME": str(root / "studio"),
},
capture_output = True,
text = True,
timeout = 60,
)
assert completed.returncode == 0, completed.stderr
assert Path(completed.stdout.strip()) == root / "whisper.cpp"
@NEEDS_POSIX_BASH
def test_the_whisper_builder_still_honours_a_studio_home_alone(tmp_path, whisper_path):
studio = tmp_path / "elsewhere" / "studio"
src = BUILD_WHISPER.read_text(encoding = "utf-8")
block = _whisper_root_block()
script = "\n".join(("set -eu", block, 'printf "%s\\n" "$INSTALL_DIR"'))
completed = subprocess.run(
["bash", "-c", script],
env = {
"HOME": str(tmp_path / "home"),
"PATH": whisper_path,
"UNSLOTH_STUDIO_HOME": str(studio),
},
capture_output = True,
text = True,
timeout = 60,
)
assert completed.returncode == 0, completed.stderr
assert Path(completed.stdout.strip()) == studio / "whisper.cpp"
@NEEDS_POSIX_BASH
def test_a_blank_master_root_does_not_outrank_the_whisper_studio_home(tmp_path, whisper_path):
"""${VAR:-} only treats the EMPTY string as unset, so an unstripped whitespace value would
win the new precedence and name a relative " /whisper.cpp"."""
studio = tmp_path / "elsewhere" / "studio"
completed = subprocess.run(
[
"bash",
"-c",
"\n".join(
(
"set -eu",
_whisper_root_block(),
'printf "%s\\n" "$INSTALL_DIR"',
)
),
],
env = {
"HOME": str(tmp_path / "home"),
"PATH": whisper_path,
"UNSLOTH_HOME": " ",
"UNSLOTH_STUDIO_HOME": str(studio),
},
capture_output = True,
text = True,
timeout = 60,
)
assert completed.returncode == 0, completed.stderr
assert Path(completed.stdout.strip()) == studio / "whisper.cpp"
@NEEDS_POSIX_BASH
def test_a_tilde_master_root_expands_for_the_whisper_builder(tmp_path, whisper_path):
home = tmp_path / "home"
completed = subprocess.run(
[
"bash",
"-c",
"\n".join(
(
"set -eu",
_whisper_root_block(),
'printf "%s\\n" "$INSTALL_DIR"',
)
),
],
env = {"HOME": str(home), "PATH": whisper_path, "UNSLOTH_HOME": "~/portable"},
capture_output = True,
text = True,
timeout = 60,
)
assert completed.returncode == 0, completed.stderr
assert Path(completed.stdout.strip()) == home / "portable" / "whisper.cpp"
@NEEDS_POSIX_BASH
def test_a_relative_master_root_resolves_against_the_caller(tmp_path):
"""Node is chosen before setup.sh's first `cd "$SCRIPT_DIR"` and llama.cpp after it, so a
value left relative names two different directories and the backend's neither."""
caller = tmp_path / "caller"
caller.mkdir()
src = SETUP_SH.read_text(encoding = "utf-8")
script = "\n".join(
(
"set -u",
_slice(src, "# Stripped before anything else", "# Directory-local evidence"),
'printf "%s\\n" "$_MASTER_ROOT"',
)
)
completed = subprocess.run(
["bash", "-c", script],
cwd = str(caller),
env = {
"HOME": str(tmp_path / "home"),
"PATH": "/usr/bin:/bin",
"PWD": str(caller),
"UNSLOTH_HOME": "not-created-yet",
"_STUDIO_HOME_IS_CUSTOM": "false",
},
capture_output = True,
text = True,
timeout = 60,
)
assert completed.returncode == 0, completed.stderr
assert Path(completed.stdout.strip()) == caller / "not-created-yet"
@NEEDS_POSIX_BASH
def test_a_master_root_alone_still_asserts_ownership_of_the_runtimes(tmp_path):
"""With only UNSLOTH_HOME set, STUDIO_HOME stays legacy and _STUDIO_HOME_IS_CUSTOM stays
false, which is what licenses the installers to os.replace() and rm -rf without checking
the Unsloth-owned marker. The runtimes moved, so the guard has to follow them."""
root = tmp_path / "portable"
root.mkdir()
home = tmp_path / "home"
(home / ".unsloth" / "studio").mkdir(parents = True)
src = SETUP_SH.read_text(encoding = "utf-8")
script = "\n".join(
(
"set -u",
_slice(src, "# Stripped before anything else", "# Directory-local evidence"),
'printf "%s %s\\n" "$_STUDIO_HOME_IS_CUSTOM" "$_RUNTIME_ROOT_IS_CUSTOM"',
)
)
completed = subprocess.run(
["bash", "-c", script],
env = {
"HOME": str(home),
"PATH": "/usr/bin:/bin",
"UNSLOTH_HOME": str(root),
"_STUDIO_HOME_IS_CUSTOM": "false",
},
capture_output = True,
text = True,
timeout = 60,
)
assert completed.returncode == 0, completed.stderr
assert completed.stdout.split() == ["false", "true"]
@NEEDS_POSIX_BASH
@pytest.mark.parametrize(
"environment, expected",
[
# Staging: STAGE_ROOT beats the master root, so the master root must not decide ownership.
pytest.param(
{"UNSLOTH_STUDIO_STAGE_ROOT": "STAGE", "_STUDIO_HOME_IS_CUSTOM": "false"},
["false", "false"],
id = "a staged update ignores the master root",
),
# The flag must be assigned, not only raised, or pre-marker legacy roots demand markers.
pytest.param(
{"UNSLOTH_HOME": "LEGACY", "_STUDIO_HOME_IS_CUSTOM": "true"},
["true", "false"],
id = "a custom studio home whose runtimes stay legacy",
),
],
)
def test_the_ownership_flag_follows_where_the_runtimes_land(tmp_path, environment, expected):
home = tmp_path / "home"
(home / ".unsloth" / "studio").mkdir(parents = True)
stage = tmp_path / "stage"
stage.mkdir()
resolved = {
key: str(stage)
if value == "STAGE"
else str(home / ".unsloth")
if value == "LEGACY"
else value
for key, value in environment.items()
}
if "UNSLOTH_HOME" not in resolved:
resolved["UNSLOTH_HOME"] = str(tmp_path / "portable")
(tmp_path / "portable").mkdir()
src = SETUP_SH.read_text(encoding = "utf-8")
script = "\n".join(
(
"set -u",
f'STAGE_ROOT="{resolved.pop("UNSLOTH_STUDIO_STAGE_ROOT", "")}"',
_slice(src, "# Stripped before anything else", "# Directory-local evidence"),
'printf "%s %s\\n" "$_STUDIO_HOME_IS_CUSTOM" "$_RUNTIME_ROOT_IS_CUSTOM"',
)
)
completed = subprocess.run(
["bash", "-c", script],
env = {"HOME": str(home), "PATH": "/usr/bin:/bin", **resolved},
capture_output = True,
text = True,
timeout = 60,
)
assert completed.returncode == 0, completed.stderr
assert completed.stdout.split() == expected
@NEEDS_POSIX_BASH
def test_no_master_root_leaves_the_ownership_flag_alone(tmp_path):
src = SETUP_SH.read_text(encoding = "utf-8")
script = "\n".join(
(
"set -u",
_slice(src, "# Stripped before anything else", "# Directory-local evidence"),
'printf "%s %s\\n" "$_STUDIO_HOME_IS_CUSTOM" "$_RUNTIME_ROOT_IS_CUSTOM"',
)
)
for flag in ("false", "true"):
completed = subprocess.run(
["bash", "-c", script],
env = {
"HOME": str(tmp_path / "home"),
"PATH": "/usr/bin:/bin",
"_STUDIO_HOME_IS_CUSTOM": flag,
},
capture_output = True,
text = True,
timeout = 60,
)
assert completed.returncode == 0, completed.stderr
assert completed.stdout.split() == [flag, flag]
def test_every_runtime_ownership_guard_uses_the_runtime_flag():
"""The Studio home and its venvs keep _STUDIO_HOME_IS_CUSTOM; every guard that names a
runtime child has to move, or a master-root install loses the marker check on it."""
src = SETUP_SH.read_text(encoding = "utf-8")
for line in src.splitlines():
if "_STUDIO_HOME_IS_CUSTOM" not in line:
continue
assert not any(
name in line for name in ("$NODE_DIR", "$LLAMA_CPP_DIR", "$WHISPER_CPP_DIR")
), line
ps = SETUP_PS1.read_text(encoding = "utf-8")
# Seeded from the Studio flag, raised only when the master root is elsewhere, as setup.sh.
assert "$RuntimeRootIsCustom = $StudioHomeIsCustom\n" in ps
assert "$_masterRootForOwnership -ine $_legacyRuntimeRoot" in ps
for line in ps.splitlines():
if "$StudioHomeIsCustom" not in line:
continue
assert not any(
name in line for name in ("$LlamaCppDir", "$WhisperCppDir", "$NodeDir")
), line
@NEEDS_POSIX_BASH
def test_the_legacy_root_named_explicitly_is_not_custom(tmp_path):
"""UNSLOTH_HOME=$HOME/.unsloth names the root a default install already uses.
Nothing moves, so demanding an owner marker there rejects an update that used to reuse a
legacy source-built ~/.unsloth/llama.cpp, which predates the marker entirely. Customness has
to come from where the runtimes LAND, not from whether the variable was set.
"""
home = tmp_path / "home"
(home / ".unsloth" / "studio").mkdir(parents = True)
src = SETUP_SH.read_text(encoding = "utf-8")
block = _slice(src, "# Stripped before anything else", "# Directory-local evidence")
script = "\n".join(
(
"set -u",
"_STUDIO_HOME_IS_CUSTOM=false",
block,
'printf "%s\\n" "$_RUNTIME_ROOT_IS_CUSTOM"',
)
)
def flag(master: str) -> str:
done = subprocess.run(
["bash", "-c", script],
env = {"HOME": str(home), "PATH": "/usr/bin:/bin", "UNSLOTH_HOME": master},
capture_output = True,
text = True,
timeout = 60,
)
assert done.returncode == 0, done.stderr
return done.stdout.strip()
assert flag(str(home / ".unsloth")) == "false"
assert flag(str(tmp_path / "portable")) == "true"
assert flag("") == "false"
@pytest.mark.skipif(PWSH is None, reason = "needs pwsh")
def test_the_windows_legacy_root_named_explicitly_is_not_custom(tmp_path):
"""The Windows half of the test above, run rather than pattern-matched.
setup.ps1 read `$StudioHomeIsCustom -or [bool](Get-MasterRootOverride)`, so
UNSLOTH_HOME=%USERPROFILE%\\.unsloth turned the ownership guard on for a root a default
install is already using. Nothing moves there, and the guard then demands an owner marker
from a markerless source-built .unsloth\\llama.cpp, so Assert-StudioOwnedOrAbsent exits an
update that used to reuse that build. setup.sh adopts it, which made the two halves answer
differently for one environment.
"""
profile = tmp_path / "profile"
(profile / ".unsloth" / "studio").mkdir(parents = True)
ps = SETUP_PS1.read_text(encoding = "utf-8")
block = _slice(ps, "$RuntimeRootIsCustom = $StudioHomeIsCustom", "$LlamaCppDir = ")
script = tmp_path / "probe.ps1"
script.write_text(
"\n".join(
(
f"$txt = Get-Content -Raw '{SETUP_PS1}'",
'foreach ($n in @("Get-PathState", "Test-AccessDeniedError", "Get-CanonicalDir",'
' "Get-MasterRootOverride")) {',
' $m = [regex]::Match($txt, "(?ms)^function $n \\{.*?^\\}")',
' if (-not $m.Success) { Write-Output "EXTRACT-FAILED:$n"; exit 1 }',
" Invoke-Expression $m.Value",
"}",
# Test-StudioHomeIsCustom closes over $StudioHome, so it is stated, not extracted.
"$StudioHomeIsCustom = $false",
block,
"Write-Output $RuntimeRootIsCustom",
)
),
encoding = "utf-8",
)
def flag(master: str) -> str:
out = run_pwsh(
[PWSH, "-NoProfile", "-File", str(script)],
env = {
"PATH": os.environ.get("PATH", ""),
"HOME": str(profile),
"USERPROFILE": str(profile),
"UNSLOTH_HOME": master,
},
capture_output = True,
text = True,
check = True,
).stdout.strip()
assert "EXTRACT-FAILED" not in out, out
return out.splitlines()[-1].strip()
assert flag(str(profile / ".unsloth")) == "False"
assert flag(str(tmp_path / "portable")) == "True"
assert flag("") == "False"
@pytest.mark.skipif(PWSH is None, reason = "needs pwsh")
def test_the_windows_uninstaller_only_removes_a_root_that_is_really_empty(tmp_path):
"""The master root and its .staging go only when empty, and by an operation that cannot do
anything else.
Windows asked with Get-ChildItem -ErrorAction SilentlyContinue and then called _RemovePath,
which is Remove-Item -Recurse -Force, in the one directory this branch teaches users to
point at their own files. Two separate calls, so a file arriving between the two was taken
by the -Recurse; and a swallowed enumeration error read as an empty directory.
uninstall.sh uses rmdir for both paths, which is one call that refuses a non-empty
directory, and this file's own %TEMP% prune already asks with -ErrorAction Stop.
The behavioural halves below are what can be shown on a POSIX host: a recursive delete needs
to enumerate too, so an unlistable directory survives either version here and the ACL shape
that separates DELETE from LIST is Windows-only. What is checked instead is the mechanism,
which is what removes the race as well.
"""
ps = UNINSTALL_PS1.read_text(encoding = "utf-8")
assert "function _RemoveDirIfEmpty" in ps
helper = _slice(ps, " function _RemoveDirIfEmpty {", "\n # The exact shape")
# One atomic call: no -Recurse, and no separate emptiness check.
assert "[System.IO.Directory]::Delete($Path, $false)" in helper
assert "-Recurse" not in helper, helper
assert "Get-ChildItem" not in helper, helper
master_block = _slice(ps, "$masterRoot = $masterRootToStop", "if ($defaultLlamaCpp)")
assert master_block.count("_RemoveDirIfEmpty") == 2, master_block
assert "-ErrorAction SilentlyContinue)) {\n _RemovePath" not in master_block
script = tmp_path / "probe.ps1"
script.write_text(
"\n".join(
(
f"$txt = Get-Content -Raw '{UNINSTALL_PS1}'",
'$m = [regex]::Match($txt, "(?ms)^ function _RemoveDirIfEmpty \\{.*?^ \\}")',
'if (-not $m.Success) { Write-Output "EXTRACT-FAILED"; exit 1 }',
"Invoke-Expression $m.Value",
"function _Substep { param($a, $b) }",
"_RemoveDirIfEmpty $args[0]",
'Write-Output ("EXISTS=" + (Test-Path -LiteralPath $args[0]))',
)
),
encoding = "utf-8",
)
def still_there(path: Path) -> bool:
out = run_pwsh(
[PWSH, "-NoProfile", "-File", str(script), str(path)],
env = {"PATH": os.environ.get("PATH", ""), "HOME": str(tmp_path)},
capture_output = True,
text = True,
check = True,
).stdout
assert "EXTRACT-FAILED" not in out, out
assert "EXISTS=" in out, out
return out.strip().splitlines()[-1] == "EXISTS=True"
empty = tmp_path / "empty"
empty.mkdir()
assert not still_there(empty), "an empty root is the one case that may be removed"
occupied = tmp_path / "occupied"
occupied.mkdir()
(occupied / "theirs.txt").write_text("mine", encoding = "utf-8")
assert still_there(occupied)
assert (occupied / "theirs.txt").is_file(), "a non-empty root must keep its contents"
# A dir holding only a subdir: rmdir refuses it, a -Recurse would not.
nested = tmp_path / "nested"
(nested / "child").mkdir(parents = True)
assert still_there(nested)
assert (nested / "child").is_dir()
@NEEDS_POSIX_BASH
def test_only_a_directory_can_be_adopted_at_a_runtime_path(tmp_path):
"""A dangling symlink and a regular file are both things the user put there.
-d alone read either as "nothing is here", and the caller then rm -rf'd the path or let
install_*_prebuilt.py os.replace() over it. A dangling link is the ordinary case: its target
volume is simply not mounted, and resolving it later installs onto somebody's other disk.
"""
src = SETUP_SH.read_text(encoding = "utf-8")
block = _slice(src, "_studio_path_shape() {", "\n_packaged_frontend_available")
script = "\n".join(
(
"set -u",
"_STUDIO_OWNED_MARKER=.unsloth-studio-owned",
"_studio_owned_adoptable() { return 1; }",
"_studio_dir_unsearchable() { return 1; }",
"_path_access_denied() { echo DENIED; exit 9; }",
"setup_fail() { echo REFUSED; exit 1; }",
block,
'_assert_studio_owned_or_absent "$1" llama.cpp true',
"echo ALLOWED",
)
)
def verdict(path: pathlib.Path) -> str:
done = subprocess.run(
["bash", "-c", script, "_", str(path)],
env = {"HOME": str(tmp_path), "PATH": "/usr/bin:/bin"},
capture_output = True,
text = True,
timeout = 60,
)
return done.stdout.strip().splitlines()[-1] if done.stdout.strip() else done.stderr[-120:]
assert verdict(tmp_path / "absent") == "ALLOWED"
dangling = tmp_path / "dangling"
dangling.symlink_to(tmp_path / "no-such-volume" / "llama.cpp")
assert verdict(dangling) == "REFUSED"
plain = tmp_path / "afile"
plain.write_text("mine")
assert verdict(plain) == "REFUSED"
owned = tmp_path / "owned"
owned.mkdir()
(owned / ".unsloth-studio-owned").touch()
assert verdict(owned) == "ALLOWED"
def test_the_windows_inductor_cache_agrees_with_the_resolver():
"""TORCHINDUCTOR_CACHE_DIR must match the resolver, except long-paths-off and space/apostrophe paths."""
ps = SETUP_PS1.read_text(encoding = "utf-8")
block = _slice(ps, "$TorchCacheDir = $null", "$env:TORCHINDUCTOR_CACHE_DIR = $TorchCacheDir")
assert 'Join-Path (Join-Path $StudioHome "cache") "torchinductor"' in block
assert "$LongPathsEnabled" in block
refusal = re.search(r"-notmatch\s+'(\[[^']*(?:''[^']*)*\])'", block)
assert refusal, "the unparseable-path refusal is gone"
assert "\\s" in refusal.group(1), "the whitespace refusal is gone"
assert "''" in refusal.group(1), "the apostrophe refusal is gone"
# The refusals differ: an apostrophe-only path publishes nothing since C:\tc is shared.
assert "$TorchCacheUnparseable" in block, "the apostrophe case lost its separate route"
assert "-not $TorchCacheUnparseable" in block
assert '"C:\\tc"' in block
roots = (REPO_ROOT / "studio" / "backend" / "utils" / "paths" / "storage_roots.py").read_text(
encoding = "utf-8"
)
# Same key named by both sides, so they cannot drift apart.
assert '"TORCHINDUCTOR_CACHE_DIR",' in roots
assert 'str(root / "torchinductor")' in roots
def test_the_windows_node_guard_covers_a_master_root():
"""The Node ownership guard and marker must also cover a master root, not only $NodeOverride."""
ps = SETUP_PS1.read_text(encoding = "utf-8")
# Keyed on $NodeOverride, not $NodeDir, whose probe moved and would pass by seeing less.
lines = ps.splitlines()
start = next(i for i, line in enumerate(lines) if "$nodeEntry = if (" in line)
guards = [line for line in lines[start:] if "$NodeOverride" in line]
# Fewer means the block was restructured and this test would pass by finding nothing.
assert len(guards) >= 2, guards
for line in guards:
assert "$RuntimeRootIsCustom" in line, line
UNINSTALL_SH = REPO_ROOT / "scripts" / "uninstall.sh"
UNINSTALL_PS1 = REPO_ROOT / "scripts" / "uninstall.ps1"
@pytest.mark.skipif(shutil.which("pwsh") is None, reason = "pwsh not available")
def test_the_windows_uninstaller_resolves_a_relative_root_like_setup(tmp_path):
"""setup.ps1 resolves through PowerShell's own location; uninstall.ps1 used
[IO.Path]::GetFullPath, which anchors at [Environment]::CurrentDirectory.
PowerShell does not keep those two in step, so after a Set-Location a relative UNSLOTH_HOME
named one install to setup and a different one to the uninstaller. The owner marker does not
save you there: it spares trees that are not Unsloth's, and the wrongly resolved path is
another Unsloth install, marker and all.
Runs the shipped function rather than matching its text, so a rewrite that keeps the words
and loses the behaviour still fails.
"""
initial = tmp_path / "initial"
chosen = tmp_path / "chosen"
(chosen / "portable").mkdir(parents = True)
initial.mkdir()
out = _master_root_answer(
tmp_path,
f"""[System.Environment]::CurrentDirectory = "{initial}"
Set-Location "{chosen}"
$env:UNSLOTH_HOME = "portable"
$env:USERPROFILE = "{tmp_path}/profile"
Write-Output (_MasterRoot)
""",
)
assert out == str(chosen / "portable"), out
def test_neither_uninstaller_recurses_into_an_install_lock_path():
"""Install lock names are always regular files; a directory there is the user's, so never recurse."""
sh = UNINSTALL_SH.read_text(encoding = "utf-8")
ps = UNINSTALL_PS1.read_text(encoding = "utf-8")
assert "_remove_lock_file() {" in sh
assert "function _RemoveLockFile" in ps
# Call sites do not all spell out the lock name, so match these variable spellings too.
lockish = ("install.lock", "lockName", "$stale", "_stale", "_mr_lock")
for text, remover, shape in (
(sh, "_remove_path ", "_remove_lock_file"),
(ps, "_RemovePath ", "_RemoveLockFile"),
):
hits = 0
for line in text.splitlines():
if line.lstrip().startswith("#"):
continue
if remover not in line and shape not in line:
continue
if not any(token in line for token in lockish):
continue
hits += 1
assert shape in line, line
# Counts sites found, not wrong ones; zero would mean the tokens stopped matching.
assert hits >= 4, (remover, hits)
assert "install.lock.stale" in ps
assert '"*.install.lock.stale.*"' not in ps
def test_both_uninstallers_clear_the_master_root_children():
"""Uninstallers must also clear the master root's runtime children, not only the legacy siblings."""
sh = UNINSTALL_SH.read_text(encoding = "utf-8")
ps = UNINSTALL_PS1.read_text(encoding = "utf-8")
assert "_master_root() {" in sh
assert "function _MasterRoot" in ps
for src, marker in ((sh, ".unsloth-studio-owned"), (ps, ".unsloth-studio-owned")):
block = _slice(src, "master root's own children", "llama.cpp build + cache")
assert marker in block, block
for child in ("llama.cpp", "node", "whisper.cpp"):
assert child in block, (child, block)
assert '_is_unsafe_root "$_mr_root"' in sh
assert "_IsUnsafeRoot $masterRoot" in ps
# Adds its studio child only when neither override is set, as storage_roots.studio_root().
assert '_emit "$(_master_root)/studio"' in sh
assert '$envRoot = (Join-Path $master "studio")' in ps
def test_the_stop_pass_covers_the_master_root_runtimes():
"""Stop only marker-owned runtimes under master root before removing; Windows locks loaded binaries."""
ps = UNINSTALL_PS1.read_text(encoding = "utf-8")
stop_line = next(l for l in ps.splitlines() if l.strip().startswith("$stopRoots = "))
assert "$masterChildrenToStop" in stop_line, stop_line
block = _slice(ps, "$masterRootToStop = _MasterRoot", "$stopRoots = ")
assert ".unsloth-studio-owned" in block, block
assert "_IsUnsafeRoot $masterRootToStop" in block, block
assert ps.index("$masterRootToStop = _MasterRoot") < ps.index(
"_StopProcessesLockingRoots -Roots"
)
def test_a_shared_staging_directory_is_pruned_not_deleted():
"""The prebuilt installers share <root>/.staging and prune it only when empty, so anything
left in a user-chosen root is not ours to delete recursively."""
sh = UNINSTALL_SH.read_text(encoding = "utf-8")
ps = UNINSTALL_PS1.read_text(encoding = "utf-8")
assert 'rmdir "$_mr_root/.staging"' in sh
assert '_remove_path "$_mr_root/.staging"' not in sh
staging = _slice(
ps, '_RemoveDirIfEmpty (Join-Path $masterRoot ".staging")', "# Shared llama.cpp build"
)
assert "_RemovePath" not in staging, staging
def test_the_windows_uninstaller_refuses_a_note_carried_in_from_elsewhere(tmp_path):
"""Refuse a note copied from another install; owner markers cannot tell the two trees apart."""
original = tmp_path / "original"
(original / "studio").mkdir(parents = True)
copied = tmp_path / "copied"
(copied / "studio" / "share").mkdir(parents = True)
(copied / "studio" / "share" / ".unsloth-master-root").write_text(
f"{original}\n", encoding = "utf-8"
)
profile = tmp_path / "profile"
(profile / ".unsloth" / "studio" / "share").mkdir(parents = True)
script = tmp_path / "probe.ps1"
script.write_text(
f"""$txt = Get-Content -Raw "{UNINSTALL_PS1}"
foreach ($n in @("_ExpandTilde", "_RootFromConf", "_MasterRoot")) {{
$m = [regex]::Match($txt, "(?ms)^ function $n \\{{.*?^ \\}}")
if (-not $m.Success) {{ Write-Output "EXTRACT-FAILED:$n"; exit 1 }}
Invoke-Expression $m.Value
}}
$env:UNSLOTH_HOME = ""
$env:STUDIO_HOME = ""
$env:UNSLOTH_STUDIO_HOME = "{copied}/studio"
$env:USERPROFILE = "{profile}"
$answer = _MasterRoot
Write-Output "ANSWER:$answer"
""",
encoding = "utf-8",
)
out = run_pwsh(
[PWSH, "-NoProfile", "-File", str(script)],
capture_output = True,
text = True,
check = True,
).stdout.strip()
assert "EXTRACT-FAILED" not in out, out
assert out == "ANSWER:", out
def test_the_windows_uninstaller_does_not_borrow_another_installs_note(tmp_path):
"""_MasterRoot must not follow a note from a legacy install when the named Studio tree has none."""
profile = tmp_path / "profile"
(profile / ".unsloth" / "studio" / "share").mkdir(parents = True)
borrowed = tmp_path / "borrowed"
(borrowed / "studio").mkdir(parents = True)
(profile / ".unsloth" / "studio" / "share" / ".unsloth-master-root").write_text(
f"{borrowed}\n", encoding = "utf-8"
)
named = tmp_path / "named"
(named / "share").mkdir(parents = True)
out = _master_root_answer(
tmp_path,
f"""$env:UNSLOTH_HOME = ""
$env:STUDIO_HOME = ""
$env:UNSLOTH_STUDIO_HOME = "{named}"
$env:USERPROFILE = "{profile}"
$answer = _MasterRoot
Write-Output "ANSWER:$answer"
""",
)
assert out == "ANSWER:", out
def test_the_windows_uninstaller_finds_a_master_root_from_the_note(tmp_path):
"""The uninstaller must read the master-root note, since an ordinary shell has no UNSLOTH_HOME set."""
master = tmp_path / "portable"
(master / "studio" / "share").mkdir(parents = True)
(master / "studio" / "share" / ".unsloth-master-root").write_text(
f"{master}\n", encoding = "utf-8"
)
profile = tmp_path / "profile"
(profile / ".unsloth" / "studio" / "share").mkdir(parents = True)
out = _master_root_answer(
tmp_path,
f"""$env:UNSLOTH_HOME = ""
$env:STUDIO_HOME = ""
$env:UNSLOTH_STUDIO_HOME = "{master}/studio"
$env:USERPROFILE = "{profile}"
Write-Output (_MasterRoot)
""",
)
assert out == str(master), out
def test_the_windows_setup_records_the_master_root_for_the_uninstaller():
"""Write the master-root note only for a real master root; elsewhere it could only go stale."""
ps = SETUP_PS1.read_text(encoding = "utf-8")
block = _slice(
ps, "# Record the master root inside the Studio tree", "$WithLlamaCppDir = $null"
)
assert "(Get-MasterRootOverride)" in block
assert "-not $StageRoot" in block
assert '".unsloth-master-root"' in block
# Staged then renamed: this note licenses deletions, so it must never be half-written.
assert "$noteTmp" in block and "Move-Item" in block
# The 3-argument overwrite overload is .NET Core only, and setup.ps1 runs under 5.1.
assert "[System.IO.File]::Move(" not in block
def test_the_refused_windows_cache_path_is_taken_away_on_upgrade():
"""A refused cache path this installer wrote must be cleared on every launch, not only in deps block."""
ps = SETUP_PS1.read_text(encoding = "utf-8")
body = _slice(
ps, "function Clear-UnparseableTorchCacheEnv {", "\nClear-UnparseableTorchCacheEnv"
)
code = "\n".join(line for line in body.splitlines() if not line.lstrip().startswith("#"))
assert "GetEnvironmentVariable('TORCHINDUCTOR_CACHE_DIR', 'User')" in code, code
assert "[NullString]::Value, 'User'" in code, code
assert "Remove-Item -LiteralPath Env:TORCHINDUCTOR_CACHE_DIR" in code, code
assert code.count("Test-UnparseableManagedTorchCache") == 2, code
predicate = _slice(
ps, "function Test-UnparseableManagedTorchCache", "\nfunction Clear-Unparseable"
)
rule = "\n".join(l for l in predicate.splitlines() if not l.lstrip().startswith("#"))
assert "-notmatch '[\\s'']'" in rule, rule
# Nothing records who set the variable, so compare against this run's managed path.
assert "$Managed" in rule and "-ieq" in rule, rule
assert '$managedTorchCache = Join-Path (Join-Path $StudioHome "cache") "torchinductor"' in code
assert "-not $StageRoot" in code, code
lines = ps.splitlines()
gate = lines.index("if (-not $SkipPythonDeps) {")
depth = 0
for close, line in enumerate(lines[gate:], start = gate):
depth += line.count("{") - line.count("}")
if depth == 0:
break
calls = [i for i, line in enumerate(lines) if line.strip() == "Clear-UnparseableTorchCacheEnv"]
assert calls, "nothing calls the cleanup"
assert any(
i < gate for i in calls
), "the cleanup only runs on a dependency pass, which the fast paths skip"
def test_the_windows_uninstaller_clears_the_inductor_path_it_persisted():
"""The uninstaller clears TORCHINDUCTOR_CACHE_DIR only when it lies inside a root this run owned."""
ps = UNINSTALL_PS1.read_text(encoding = "utf-8")
block = _slice(
ps, "# Clear the persisted Inductor cache path", "# Remove HKCU\\Software\\Unsloth"
)
assert "GetEnvironmentVariable('TORCHINDUCTOR_CACHE_DIR', 'User')" in block
assert "[NullString]::Value, 'User'" in block
# $knownRoots includes roots the gates refused to delete.
assert "$ownedRoots" in block and "$knownRoots" not in block
# Strip comments first: prose naming C:\tc is not code naming it.
code = "\n".join(line for line in block.splitlines() if not line.lstrip().startswith("#"))
assert "C:\\tc" not in code
def test_the_windows_node_guard_treats_a_file_as_occupied():
"""Files and symlinks count as occupied, since ownership evidence lives inside directories."""
ps = SETUP_PS1.read_text(encoding = "utf-8")
block = _slice(
ps,
" $nodeEntry = if ($NodeOverride -or $RuntimeRootIsCustom) {",
"install_node_prebuilt.py",
)
code = "\n".join(line for line in block.splitlines() if not line.lstrip().startswith("#"))
# Get-Item -Force, not Test-Path: 5.1 Test-Path follows links, so a dangling one reads absent.
assert "Get-Item -LiteralPath $NodeDir -Force" in code, code
assert "Test-Path -LiteralPath $NodeDir" not in code, code
assert "$nodeEntry.PSIsContainer" in code and "ReparsePoint" in code, code
assert "$nodeIsDir" in code and "-not $nodeIsDir -or" in code
def test_the_windows_runtime_guard_treats_a_file_as_occupied():
"""Assert-StudioOwnedOrAbsent must treat a regular file as occupied, not only a directory."""
ps = SETUP_PS1.read_text(encoding = "utf-8")
block = _slice(ps, "function Assert-StudioOwnedOrAbsent", "function Mark-StudioOwned")
code = "\n".join(line for line in block.splitlines() if not line.lstrip().startswith("#"))
assert "Get-Item -LiteralPath $Path -Force" in code
assert "if (-not $isCustomRoot) { return }" in code
assert "already exists and is not a directory" in block
# Fatal even in -NonFatal mode: that mode rescues a denial, not somebody else's file.
refusal = code[
code.index("Get-Item -LiteralPath $Path -Force") : code.index("Exit-SetupFailure")
]
assert "$NonFatal" not in refusal, refusal
def test_neither_uninstaller_takes_a_studio_root_that_is_also_the_master_root():
"""Never delete a Studio root that is also the master root; a whole-directory delete takes user
files."""
sh = UNINSTALL_SH.read_text(encoding = "utf-8")
ps = UNINSTALL_PS1.read_text(encoding = "utf-8")
sh_block = _slice(sh, "_crf_canon=", '_remove_root_recording_db "$_custom_root"')
sh_code = "\n".join(l for l in sh_block.splitlines() if not l.lstrip().startswith("#"))
assert "_MASTER_ROOT_SAVED" in sh_code and "continue" in sh_code
# Canonicalised on both sides, or a symlinked path compares unequal to itself.
assert "cd -P --" in sh_code
ps_block = _slice(ps, "$flatMaster = $masterRootToStop", "_RemoveRootRecordingDb $r")
ps_code = "\n".join(l for l in ps_block.splitlines() if not l.lstrip().startswith("#"))
assert "$masterRootToStop" in ps_code and "continue" in ps_code
# Case-insensitive: Windows paths differing only in case are the same directory.
assert "-ieq" in ps_code
def test_neither_uninstaller_re_resolves_the_master_root_after_deleting_it():
"""Resolve the master root once before deleting anything, since its note lives inside a Studio tree."""
sh = UNINSTALL_SH.read_text(encoding = "utf-8")
ps = UNINSTALL_PS1.read_text(encoding = "utf-8")
sh_code = "\n".join(l for l in sh.splitlines() if not l.lstrip().startswith("#"))
saved = sh_code.index('_MASTER_ROOT_SAVED="$(_master_root)"')
removal = sh_code.index("_custom_studio_roots | while IFS= read -r _custom_root")
# Resolved before the first deletion; calls before the loop are fine.
assert saved < removal
after = sh_code[removal:]
assert "_master_root" not in after, after[: after.index("_master_root") + 200]
assert after.count('"$_MASTER_ROOT_SAVED"') >= 2
ps_code = "\n".join(l for l in ps.splitlines() if not l.lstrip().startswith("#"))
ps_saved = ps_code.index("$masterRootToStop = _MasterRoot")
ps_removal = ps_code.index("_RemoveRootRecordingDb $r")
assert ps_saved < ps_removal
ps_after = ps_code[ps_removal:]
assert "_MasterRoot" not in ps_after, ps_after[: ps_after.index("_MasterRoot") + 200]
assert ps_after.count("$masterRootToStop") >= 1
def test_neither_uninstaller_sweeps_a_stale_lock_name_it_did_not_make():
"""prebuilt_core.py leaves <name>.stale.<pid>. A dotted glob also matched a user's own
".backup.install.lock.stale.copy", and in a root the user chose that file is theirs.
Behaviour for the POSIX half is in tests/sh/test_uninstall_master_root.sh."""
sh = UNINSTALL_SH.read_text(encoding = "utf-8")
ps = UNINSTALL_PS1.read_text(encoding = "utf-8")
sh_code = "\n".join(l for l in sh.splitlines() if not l.lstrip().startswith("#"))
assert ".*.install.lock.stale.*" not in sh_code
assert sh_code.count("*[!0-9]*) continue ;;") >= 2
ps_code = "\n".join(l for l in ps.splitlines() if not l.lstrip().startswith("#"))
assert '-like ".*.install.lock.stale.*"' not in ps_code
assert "$script:StaleLockPattern" in ps_code
assert "[0-9]+$" in ps_code
@NEEDS_POSIX_BASH
def test_the_master_root_note_does_not_write_through_a_planted_link(tmp_path):
"""The note is staged under an unpredictable name, so a link left in share/ is not followed.
The staging name used to be "$notePath.$$", and a shell redirection into an existing symlink
truncates its target. Anyone who could write share/ -- a portable volume mounted for two
accounts is the shape that matters, since a master root is what this note records -- could
therefore point that name at a file of theirs and have setup empty it. The attacker's
capability is simulated exactly rather than approximated: the link is created by the same
shell that then runs the shipped block, so "$$" matches the way a guessed pid would.
setup.sh already states this rule for the uv cache probe ("mktemp (O_EXCL) not $$"); the note
writer is now held to it too.
"""
src = SETUP_SH.read_text(encoding = "utf-8")
block = _slice(src, "_master_root_note_is_honoured() {", "LLAMA_CPP_DIR=")
# Contained, so the honoured-note gate passes and the writer actually runs.
master = tmp_path / "portable"
studio_home = master / "studio"
(studio_home / "share").mkdir(parents = True)
victim = tmp_path / "victim.txt"
victim.write_text("do not truncate me\n", encoding = "utf-8")
script = "\n".join(
(
"set -u",
'ln -s "$VICTIM" "$STUDIO_HOME/share/.unsloth-master-root.$$"',
block,
)
)
completed = subprocess.run(
["bash", "-c", script],
env = {
"PATH": os.environ.get("PATH", "/usr/bin:/bin"),
"STUDIO_HOME": str(studio_home),
"UNSLOTH_HOME": str(master),
"_MASTER_ROOT": str(master),
"STAGE_ROOT": "",
"VICTIM": str(victim),
},
capture_output = True,
text = True,
timeout = 60,
)
assert completed.returncode == 0, completed.stderr
assert victim.read_text(encoding = "utf-8") == "do not truncate me\n"
note = studio_home / "share" / ".unsloth-master-root"
assert note.read_text(encoding = "utf-8").strip() == str(master)
assert not note.is_symlink()
leftovers = sorted(
p.name
for p in (studio_home / "share").iterdir()
if p.name.startswith(".unsloth-master-root.") and not p.is_symlink()
)
assert leftovers == [], leftovers
def test_the_windows_note_writer_creates_its_staging_file_exclusively():
"""The setup.ps1 half of the test above. Held structurally: the Linux runners have no
Windows filesystem to plant a link on, and CreateNew is the thing that must not regress."""
ps = SETUP_PS1.read_text(encoding = "utf-8")
block = _slice(ps, "if ((Get-MasterRootOverride) -and -not $StageRoot -and", "\n# ")
# Comments stripped: the block mentions the old staging name in prose.
block = "\n".join(l for l in block.splitlines() if not l.lstrip().startswith("#"))
assert "$notePath.$PID" not in block, "predictable staging name is back"
assert "GetRandomFileName()" in block
assert "[System.IO.FileMode]::CreateNew" in block
# WriteAllText opens an existing path rather than failing on it.
assert "WriteAllText($noteTmp" not in block
def _run_note_block(
tmp_path,
studio_home,
master,
*,
existing_note = None,
home = None,
):
"""Executes the note gate, writer and legacy sweep as one block, since they are one decision."""
src = SETUP_SH.read_text(encoding = "utf-8")
block = _slice(src, "_master_root_note_is_honoured() {", "LLAMA_CPP_DIR=")
note = studio_home / "share" / ".unsloth-master-root"
note.parent.mkdir(parents = True, exist_ok = True)
if existing_note is not None:
note.write_text(str(existing_note) + "\n", encoding = "utf-8")
completed = subprocess.run(
["bash", "-c", "set -u\n" + block],
env = {
"PATH": os.environ.get("PATH", "/usr/bin:/bin"),
"HOME": str(home if home is not None else tmp_path / "home"),
"STUDIO_HOME": str(studio_home),
"UNSLOTH_HOME": str(master),
"_MASTER_ROOT": str(master),
"STAGE_ROOT": "",
},
capture_output = True,
text = True,
timeout = 60,
)
assert completed.returncode == 0, completed.stderr
value = note.read_text(encoding = "utf-8").strip() if note.exists() else None
return value, completed.stderr
@NEEDS_POSIX_BASH
def test_the_master_root_note_is_only_written_when_a_reader_honours_it(tmp_path):
"""A note whose Studio tree is not INSIDE the root it names is refused by every reader --
storage_roots.py, the CLI and both uninstallers all require containment -- so writing one
records nothing and hides the fact that nothing was recorded.
This is not a corner. install.sh does not read UNSLOTH_HOME yet, so
`UNSLOTH_HOME=/mnt/portable unsloth studio update` on an ordinary install is exactly this
shape: Studio stays at ~/.unsloth/studio while llama.cpp, node and whisper.cpp go to
/mnt/portable. That is the case the note was added for, and the case in which it was written
and then thrown away by every reader. Warn instead.
"""
home = tmp_path / "home"
studio_home = home / ".unsloth" / "studio"
master = tmp_path / "portable"
master.mkdir()
value, stderr = _run_note_block(tmp_path, studio_home, master, home = home)
assert value is None, f"wrote a note no reader will honour: {value}"
assert "cannot be recorded" in stderr, stderr
contained = master / "studio"
value, _ = _run_note_block(tmp_path, contained, master, home = home)
assert value == str(master)
@NEEDS_POSIX_BASH
def test_the_legacy_default_root_is_never_recorded_and_an_old_note_is_cleared(tmp_path):
"""`UNSLOTH_HOME=$HOME/.unsloth` names the directory the install is already in. Both
uninstallers refuse that value outright, so a note carrying it licenses nothing -- but
storage_roots honours it, and honouring it flips portable_mode() on for every later BARE
launch, which moves HF_HUB_CACHE off ~/.cache/huggingface. One command that named the default
root therefore turned a shared model cache into a private one, permanently, with no variable
set at the time.
The write is refused, and an existing note carrying that value is cleared on the next run --
without the sweep the damage survives the fix, since the writer only runs when UNSLOTH_HOME
is set again and the user who hit this set it once.
"""
home = tmp_path / "home"
studio_home = home / ".unsloth" / "studio"
legacy_master = home / ".unsloth"
studio_home.mkdir(parents = True)
# Contained (studio is inside ~/.unsloth), so only the legacy-root rule can refuse it.
value, _ = _run_note_block(tmp_path, studio_home, legacy_master, home = home)
assert value is None, f"recorded the legacy default root: {value}"
value, _ = _run_note_block(
tmp_path,
studio_home,
legacy_master,
existing_note = legacy_master,
home = home,
)
assert value is None, "a stale legacy-root note survived"
# A note naming another root is left alone: it may describe an install this run cannot see.
other = tmp_path / "portable"
other.mkdir()
value, _ = _run_note_block(
tmp_path,
studio_home,
legacy_master,
existing_note = other,
home = home,
)
assert value == str(other)
def test_the_windows_note_gate_holds_the_same_two_rules():
"""The setup.ps1 half of the two tests above. Structural: no PowerShell-on-Linux run can
exercise %USERPROFILE% canonicalisation the way a real Windows profile does."""
ps = SETUP_PS1.read_text(encoding = "utf-8")
gate = _slice(ps, "function Test-MasterRootNoteIsHonoured {", "\nif ((Get-MasterRootOverride)")
assert "$legacy = Get-CanonicalDir" in gate, "the legacy default root is not refused"
assert "StartsWith($norm + $sep" in gate, "containment is not checked"
assert "Test-MasterRootNoteIsHonoured -Root $UnslothHome -StudioRoot $StudioHome" in ps
sweep = _slice(ps, "$staleNote = Join-Path", "} catch {")
assert "Remove-Item -LiteralPath $staleNote" in sweep
def test_the_windows_note_writer_is_a_no_op_when_the_note_already_says_this():
"""Skip the write when the note already matches: Move-Item -Force is not atomic and can leave no
note."""
ps = SETUP_PS1.read_text(encoding = "utf-8")
block = _slice(
ps, ' $notePath = Join-Path $noteDir ".unsloth-master-root"', "\n } catch {"
)
assert "Remove-Item -LiteralPath $noteTmp" in block, "the staging file is never cleaned up"
assert "$noteSame" in block, "the note is rewritten even when it already says this"
@pytest.mark.skipif(PWSH is None, reason = "needs pwsh")
def test_the_windows_note_writer_rewrites_only_when_the_value_changed(tmp_path):
"""The behaviour, measured: same value twice leaves the file untouched; a different value
replaces it; and no staging file survives either way."""
ps = SETUP_PS1.read_text(encoding = "utf-8")
block = _slice(
ps, ' $notePath = Join-Path $noteDir ".unsloth-master-root"', "\n } catch {"
)
note_dir = tmp_path / "share"
note_dir.mkdir()
script = tmp_path / "write_note.ps1"
script.write_text(
f"$noteDir = {_ps_quote(str(note_dir))}\n"
f"$UnslothHome = {_ps_quote(str(tmp_path / 'master'))}\n"
+ block
+ "\n$final = Join-Path $noteDir '.unsloth-master-root'\n"
"$stamp = (Get-Item -LiteralPath $final).LastWriteTimeUtc.Ticks\n"
"Write-Output ((Get-Content -LiteralPath $final -Raw) + '|' + $stamp)\n",
encoding = "utf-8",
)
argv = [PWSH, "-NoProfile", "-File", str(script)]
first = run_pwsh(argv, capture_output = True, text = True, check = True)
note = note_dir / ".unsloth-master-root"
# Back-dated: two runs a millisecond apart can land on one filesystem tick.
old_stamp = 1_000_000_000
os.utime(note, (old_stamp, old_stamp))
second = run_pwsh(argv, capture_output = True, text = True, check = True)
body_1, _ = first.stdout.strip().rsplit("|", 1)
body_2, _ = second.stdout.strip().rsplit("|", 1)
assert body_1 == body_2 == str(tmp_path / "master") + "\n"
assert (
int(note.stat().st_mtime) == old_stamp
), "an unchanged note was rewritten, re-entering the delete-then-move window"
leftovers = [p.name for p in note_dir.iterdir() if p.name != ".unsloth-master-root"]
assert leftovers == [], leftovers
# A changed value must still be written, or the check above passes by doing nothing.
script.write_text(
script.read_text(encoding = "utf-8").replace(
_ps_quote(str(tmp_path / "master")), _ps_quote(str(tmp_path / "moved"))
),
encoding = "utf-8",
)
run_pwsh(argv, capture_output = True, text = True, check = True)
assert note.read_text(encoding = "utf-8").strip() == str(tmp_path / "moved")
assert int(note.stat().st_mtime) != old_stamp