unsloth/studio/backend/tests/test_setup_llama_cpp_backend.py
Daniel Han 07df95079e
Studio: route every Windows installer line through the UTF-8 stdout sink (#8148)
* Studio: route every Windows setup line through the UTF-8 stdout sink

The desktop setup log rendered "?? Unsloth Studio Setup" over a rule of
replacement characters. Tauri spawns Windows PowerShell 5.1 with
CREATE_NO_WINDOW (install.rs), so the [Console]::OutputEncoding setter
throws and both entry scripts rebind [Console]::Out to a UTF-8 writer.
step/substep already write only through that writer when stdout is
redirected, so they came out right. Every other line did not: Write-Host
is written by 5.1's console host with its own writer on the OEM code
page, and U+1F9A5 has no OEM form while U+2500 becomes a bare 0xC4, which
from_utf8_lossy turns into U+FFFD. The banner and the footer are not
steps, so they kept arriving as mojibake, and install.ps1 had neither the
IsOutputRedirected probe nor a mirror at all.

Add Write-StudioLine above the first write in studio/setup.ps1 and
install.ps1: console handle when redirected, Write-Host when interactive,
since it is the only writer that colorizes. Rewrite 164 call sites in
setup.ps1 and 155 in install.ps1 onto it, including install.ps1's own
step/substep. Write-Host now survives only inside helpers that have
already ruled out the redirected sink, and the launcher script install.ps1
generates keeps its own, since it runs as a separate process.

No behaviour change for an interactive console user: same text, same
colors, same single record per line.

test_windows_setup_output_encoding.py gains byte-level coverage that the
real banner and footer, sliced out of setup.ps1, survive both launch
shapes as valid UTF-8 exactly once, plus a source contract that runs on
Linux and names any file:line that reaches for Write-Host outside the
allow-list. Studio.Setup.Output.Tests.ps1 covers Write-StudioLine in both
modes and pins install.ps1's copy to setup.ps1's.

Harnesses that splice these scripts apart now stub or dot-source
Write-StudioLine: two PowerShell harnesses, one Python harness, and the
VC++ redist leg of studio-windows-inference-smoke.

pytest tests/python tests/test_installer_*.py: 1077 passed (2 pre-existing
sandbox failures unrelated to this change). All 16 tests/studio harnesses
and 57 Pester cases pass. Both scripts parse clean.

* CI: spawn install.ps1 as a child process so its lines reach install.log

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Stub the output sink in the llama.cpp backend PowerShell harness

* Guard the console-less spawn on a Windows runner

The byte-level cases in this file run with a console attached, and a GitHub
runner gives a CREATE_NO_WINDOW child one, so the UTF-8 setter succeeds there
and every version of these scripts emits a clean banner. Those cases cannot
tell this fix from what preceded it.

Add cases that call FreeConsole() in the child first, which is the state
install.rs's own comment assumes CREATE_NO_WINDOW produces. There Write-Host
has no screen buffer to query, throws, and takes the script down: 2 bytes of
stdout and exit 1 rather than the banner. The probe is assembled entirely out
of text sliced from the script under test and spawned with install.rs's own
interpreter, flags and creation flags.

No Windows job ran this file, so its byte-level half was only ever exercised
under pwsh 7 on the Linux Backend CI leg, which is UTF-8 by default. Add it to
the cross-platform parity matrix, which already has a windows-latest row and
already triggers on install.ps1 and studio/setup.ps1.

* Report skips in the parity step

A platform-gated case that stopped running on the row it exists for still
reports green with -q alone.

* Slice the error preference too

It is what turns the Write-Host throw into a dead script rather than a
skipped line, so restating it would be assuming the result.

* Say what the comments actually mean

* Make the console-less cases fail on a lost banner, not just a mangled one

* Stub the output sink in every harness that splices these scripts

The Write-Host rewrite left four spliced-source harnesses reaching
Write-StudioLine without defining it. An undefined command is a terminating
error, so each one either aborted or was swallowed by the harness's own catch,
and the test kept passing while no longer testing anything.

- test_windows_python_venv_hardening.py, partial-rollback case: the five-line
  split-move warning was lost. The assertion that "both halves are named" only
  stayed green because $existing is a prefix of the rollback dir, so it matched
  the dir= line instead. Pin it to the warning text.
- test_path_probe_access_denied.ps1, ownership guard: the catch scored the
  command-not-found as the intended failure and never reached Exit-SetupFailure.
  Pin the check to the EXIT-SETUP message.
- test_windows_installer_concurrency_guard.py: the decision block prints before
  Exit-InstallFailure, so on Windows the active case aborted at exit 1 and never
  produced RESULT:blocked.
- Studio.Setup.Vs2026.Tests.ps1: on a host without cmake,
  Ensure-BuildToolsForLlamaSourceBuild hits the sink first and the no-op case
  fails on the throw.

Also stub the three remaining harnesses that splice sink-calling helpers but do
not reach the sink on the paths they exercise today, so the next case added to
them cannot reintroduce this.

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-08-08 06:47:03 -07:00

395 lines
15 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
"""Backend selector coverage for setup.sh and setup.ps1.
cpu maps to install_llama_prebuilt.py's persisted --force-cpu option. vulkan is
accepted and passed through in the environment for the installer to consume.
The match is case-insensitive and whitespace-trimmed, unknown values warn, and
macOS warns for the CPU-only choice.
"""
import os
import re
import shutil
import subprocess
from pathlib import Path
import pytest
_STUDIO = Path(__file__).resolve().parents[2]
_SETUP_SH = _STUDIO / "setup.sh"
_SETUP_PS1 = _STUDIO / "setup.ps1"
_SKIP_NO_BASH = pytest.mark.skipif(shutil.which("bash") is None, reason = "bash unavailable")
_SKIP_NO_PWSH = pytest.mark.skipif(shutil.which("pwsh") is None, reason = "pwsh unavailable")
def _backend_block() -> str:
text = _SETUP_SH.read_text(encoding = "utf-8")
m = re.search(r"_llama_backend=.*?esac", text, re.DOTALL)
assert m, "UNSLOTH_LLAMA_CPP_BACKEND block not found in setup.sh"
return m.group(0)
def _run(value: str | None, system: str = "Linux") -> tuple[list[str], str]:
# Pass the value through env (not the script text) so whitespace survives, and
# stub the setup.sh logging helpers the unknown-value branch calls. system sets
# _HOST_SYSTEM so the macOS (Darwin) no-op branch can be exercised.
env = {
k: v
for k, v in os.environ.items()
if k not in ("UNSLOTH_LLAMA_CPP_BACKEND", "UNSLOTH_FORCE_VULKAN")
}
if value is not None:
env["UNSLOTH_LLAMA_CPP_BACKEND"] = value
harness = (
f'set -u\n_PREBUILT_CMD=()\nC_WARN=""\nC_OK=""\n_HOST_SYSTEM="{system}"\n'
'_source_backend_choice="$(printf \'%s\' "${UNSLOTH_LLAMA_CPP_BACKEND:-auto}" '
"| awk '{$1=$1; print tolower($0)}')\"\n"
'_source_legacy_force_vulkan="$(printf \'%s\' "${UNSLOTH_FORCE_VULKAN:-}" '
"| awk '{$1=$1; print tolower($0)}')\"\n"
'step() { printf "STEP: %s\\n" "$*" >&2; }\n'
f"{_backend_block()}\n"
'printf "%s\\n" "${_PREBUILT_CMD[@]}"'
)
out = subprocess.run(
["bash", "-c", harness], capture_output = True, text = True, env = env, check = True
)
return out.stdout.split(), out.stderr
@_SKIP_NO_BASH
@pytest.mark.parametrize("value", ["cpu", "CPU", "Cpu", " cpu ", "CPU\t"])
def test_backend_cpu_appends_flag(value):
# A deliberate CPU choice persists, so it uses --force-cpu (not the transient
# --cpu-fallback the arm64 GPU-build recovery uses).
args, stderr = _run(value)
assert "--force-cpu" in args
assert "--cpu-fallback" not in args
assert "Ignoring" not in stderr
@_SKIP_NO_BASH
@pytest.mark.parametrize("value", ["cpu", "CPU", " cpu "])
def test_backend_cpu_macos_warns_no_flag(value):
# macOS has no CPU-only bundle (the universal build already runs on CPU), so the
# override warns instead of writing a misleading forced-CPU marker.
args, stderr = _run(value, system = "Darwin")
assert "--force-cpu" not in args
assert "--cpu-fallback" not in args
assert "macOS" in stderr
@_SKIP_NO_BASH
@pytest.mark.parametrize("value", [None, "", "auto", "AUTO", " "])
def test_backend_auto_no_flag_no_warn(value):
args, stderr = _run(value)
assert "--force-cpu" not in args
assert "Ignoring" not in stderr
@_SKIP_NO_BASH
@pytest.mark.parametrize("value", ["vulkan", "VULKAN", " vulkan "])
def test_backend_vulkan_is_accepted(value):
args, stderr = _run(value)
assert "--force-cpu" not in args
assert args[-2:] == ["--llama-backend", "vulkan"]
assert "Ignoring" not in stderr
@_SKIP_NO_BASH
@pytest.mark.parametrize("value", ["hip", "HIP", "rocm", " ROCM "])
def test_backend_hip_opt_out_is_accepted(value):
args, stderr = _run(value)
assert "--force-cpu" not in args
assert "--llama-backend" not in args
assert "Ignoring" not in stderr
@_SKIP_NO_BASH
@pytest.mark.parametrize("value", ["gpu", "cuda"])
def test_backend_unknown_warns_and_no_flag(value):
args, stderr = _run(value)
assert "--force-cpu" not in args
assert "Ignoring" in stderr
@_SKIP_NO_BASH
def test_arm64_recovery_uses_transient_cpu_fallback():
# The arm64 Linux GPU-build recovery must stay transient (--cpu-fallback), never
# the persisted --force-cpu, so a later update can still heal to a GPU bundle (#6097).
text = _SETUP_SH.read_text(encoding = "utf-8")
m = re.search(r"_ARM64_CPU_CMD=\((.*?)\)", text, re.DOTALL)
assert m, "arm64 CPU recovery command not found in setup.sh"
block = m.group(1)
assert "--cpu-fallback" in block
assert "--force-cpu" not in block
def test_explicit_vulkan_prebuilt_failure_does_not_change_backend():
sh = _SETUP_SH.read_text(encoding = "utf-8")
failure = sh.index('step "llama.cpp" "prebuilt install failed"')
source_build = sh.index("_NEED_LLAMA_SOURCE_BUILD=true", failure)
guard = sh.index('if [ "$_explicit_vulkan_backend" = true ]', failure)
assert guard < source_build
guarded = sh[guard:source_build]
assert "will not substitute a ROCm or CPU source build" in guarded
# Aborts through the helper, which exits AND emits [TAURI:ERROR] in desktop mode.
assert "setup_fail 1" in guarded
ps1 = _SETUP_PS1.read_text(encoding = "utf-8")
failure = ps1.index('step "llama.cpp" "prebuilt install failed"')
source_build = ps1.index("$NeedLlamaSourceBuild = $true", failure)
guard = ps1.index("if ($explicitVulkanBackend)", failure)
assert guard < source_build
guarded = ps1[guard:source_build]
assert "will not substitute a CUDA, ROCm, or CPU source build" in guarded
assert "Exit-SetupFailure" in guarded
def test_explicit_vulkan_source_build_fails_closed():
sh = _SETUP_SH.read_text(encoding = "utf-8")
local_branch = sh.index('if [ "$_LOCAL_LLAMA_CPP_LINKED" = true ]; then')
prebuilt_branch = sh.index('elif [ "$_LLAMA_FORCE_COMPILE" = "1" ]; then', local_branch)
guarded = sh[local_branch:prebuilt_branch]
assert (
'elif [ "$_explicit_vulkan_source_build" = true ] && '
'[ "$_NEED_LLAMA_SOURCE_BUILD" = true ]; then'
) in guarded
assert "Vulkan source builds are not supported" in guarded
assert "setup_fail 1" in guarded
ps1 = _SETUP_PS1.read_text(encoding = "utf-8")
local_branch = ps1.index("if ($LocalLlamaCppLinked) {")
prebuilt_branch = ps1.index(
'} elseif ($env:UNSLOTH_LLAMA_FORCE_COMPILE -eq "1") {', local_branch
)
guarded = ps1[local_branch:prebuilt_branch]
assert "} elseif ($explicitVulkanSourceBuild -and $NeedLlamaSourceBuild) {" in guarded
assert "Vulkan source builds are not supported" in guarded
assert "Exit-SetupFailure" in guarded
def test_force_compile_sets_need_source_build_before_vulkan_guard():
# UNSLOTH_LLAMA_FORCE_COMPILE=1 combined with an explicit Vulkan backend must
# hit the "Vulkan source builds are not supported" rejection, not silently
# fall through to a CUDA/ROCm/CPU source build. That only holds if
# _NEED_LLAMA_SOURCE_BUILD/$NeedLlamaSourceBuild is already true by the time
# the explicit-Vulkan elif guard runs, i.e. set earlier in the script.
sh = _SETUP_SH.read_text(encoding = "utf-8")
force_compile_set = sh.index(
'if [ "$_LLAMA_FORCE_COMPILE" = "1" ]; then\n _NEED_LLAMA_SOURCE_BUILD=true'
)
vulkan_guard = sh.index('elif [ "$_explicit_vulkan_source_build" = true ] && ')
assert force_compile_set < vulkan_guard
ps1 = _SETUP_PS1.read_text(encoding = "utf-8")
force_compile_set = ps1.index(
'if ($env:UNSLOTH_LLAMA_FORCE_COMPILE -eq "1") {\n $NeedLlamaSourceBuild = $true'
)
vulkan_guard = ps1.index("} elseif ($explicitVulkanSourceBuild -and $NeedLlamaSourceBuild) {")
assert force_compile_set < vulkan_guard
def test_legacy_force_vulkan_gets_the_same_strict_fallback():
sh = _SETUP_SH.read_text(encoding = "utf-8")
assert "_legacy_force_vulkan=" in sh
assert "1|true|yes|on) _explicit_vulkan_backend=true" in sh
ps1 = _SETUP_PS1.read_text(encoding = "utf-8")
assert "$legacyForceVulkan = $sourceLegacyForceVulkan" in ps1
assert '$legacyForceVulkan -in @("1", "true", "yes", "on")' in ps1
def _source_backend_choice_block() -> str:
text = _SETUP_SH.read_text(encoding = "utf-8")
m = re.search(
r'_source_backend_choice="\$\(printf.*?\n.*?_explicit_vulkan_source_build=false\n'
r".*?\nfi\n",
text,
re.DOTALL,
)
assert m, "_source_backend_choice block not found in setup.sh"
return m.group(0)
@_SKIP_NO_BASH
@pytest.mark.parametrize(
"backend, force_vulkan, expected_backend, expected_explicit",
[
(None, None, "auto", "false"),
("vulkan", None, "vulkan", "true"),
("auto", "on", "auto", "true"),
("banana", "1", "banana", "true"),
("cpu", "1", "cpu", "false"),
("hip", "1", "hip", "false"),
("rocm", "1", "rocm", "false"),
],
)
def test_llama_backend_source_choice_in_setup_sh(
backend, force_vulkan, expected_backend, expected_explicit
):
env = {
k: v
for k, v in os.environ.items()
if k not in ("UNSLOTH_LLAMA_CPP_BACKEND", "UNSLOTH_FORCE_VULKAN")
}
if backend is not None:
env["UNSLOTH_LLAMA_CPP_BACKEND"] = backend
if force_vulkan is not None:
env["UNSLOTH_FORCE_VULKAN"] = force_vulkan
harness = (
"set -u\n_HOST_SYSTEM=Linux\n"
f"{_source_backend_choice_block()}\n"
'printf "%s:%s" "$_source_backend_choice" "$_explicit_vulkan_source_build"'
)
out = subprocess.run(
["bash", "-c", harness], capture_output = True, text = True, env = env, check = True
)
assert out.stdout == f"{expected_backend}:{expected_explicit}"
def _ps1_search(pattern: str, flags = 0) -> str:
m = re.search(pattern, _SETUP_PS1.read_text(encoding = "utf-8"), flags)
assert m, f"setup.ps1 block not found: {pattern}"
return m.group(0)
@_SKIP_NO_PWSH
@pytest.mark.parametrize(
"backend, force_vulkan, expected_explicit",
[
(None, None, "False"),
("vulkan", None, "True"),
("auto", "on", "True"),
("cpu", "1", "False"),
("hip", "1", "False"),
("rocm", "1", "False"),
],
)
def test_llama_backend_source_choice_in_setup_ps1(backend, force_vulkan, expected_explicit):
normalize = _ps1_search(
r'\$sourceLlamaBackend = "\$\(\$env:UNSLOTH_LLAMA_CPP_BACKEND\)".*?'
r"\$explicitVulkanSourceBuild = \(.*?\n\)\n",
re.DOTALL,
)
env = {
k: v
for k, v in os.environ.items()
if k not in ("UNSLOTH_LLAMA_CPP_BACKEND", "UNSLOTH_FORCE_VULKAN")
}
if backend is not None:
env["UNSLOTH_LLAMA_CPP_BACKEND"] = backend
if force_vulkan is not None:
env["UNSLOTH_FORCE_VULKAN"] = force_vulkan
out = subprocess.run(
[
"pwsh",
"-NoProfile",
"-Command",
# Brace the name: PowerShell reads "$var:" as a scope qualifier and
# fails to parse, so the probe never ran on a host that has pwsh.
f'{normalize}\n"RESULT:${{sourceLlamaBackend}}:$explicitVulkanSourceBuild"',
],
capture_output = True,
text = True,
env = env,
check = True,
)
expected_backend = (backend or "").strip().lower()
assert out.stdout.strip() == f"RESULT:{expected_backend}:{expected_explicit}"
def _run_ps1(value: str | None) -> str:
# One snippet, not two: NORMALIZE already spans the whole if/elseif chain up to
# and including the "Ignoring UNSLOTH_LLAMA_CPP_BACKEND" warn, so the --force-cpu
# append is inside it. Composing the apply snippet on top of it made the harness
# emit the flag twice while setup.ps1 appends it once.
normalize = _ps1_search(
r"\$llamaBackend = \$sourceLlamaBackend.*?Ignoring UNSLOTH_LLAMA_CPP_BACKEND.*?\n\s*\}",
re.DOTALL,
)
assert normalize.count('$prebuiltArgs += "--force-cpu"') == 1, normalize
env = {
k: v
for k, v in os.environ.items()
if k not in ("UNSLOTH_LLAMA_CPP_BACKEND", "UNSLOTH_FORCE_VULKAN")
}
if value is not None:
env["UNSLOTH_LLAMA_CPP_BACKEND"] = value
harness = (
# The spliced snippet warns through setup.ps1's output sink, which the real
# script defines above every call site. Stub it to Write-Host so the warning
# lands on stdout, where the assertions below read it: without this the call
# errors out and the harness returns a bare "ARGS:".
"function Write-StudioLine { param([string]$Message, [string]$ForegroundColor) "
"Write-Host $Message }\n"
"$prebuiltArgs = @()\n"
'$sourceLlamaBackend = "$($env:UNSLOTH_LLAMA_CPP_BACKEND)".Trim().ToLowerInvariant()\n'
'$sourceLegacyForceVulkan = "$($env:UNSLOTH_FORCE_VULKAN)".Trim().ToLowerInvariant()\n'
f'{normalize}\n"ARGS:" + ($prebuiltArgs -join ",")'
)
out = subprocess.run(
["pwsh", "-NoProfile", "-Command", harness],
capture_output = True,
text = True,
env = env,
check = True,
)
return out.stdout
@_SKIP_NO_PWSH
@pytest.mark.parametrize("value", ["cpu", "CPU", "Cpu", " cpu ", "CPU\t"])
def test_ps1_backend_cpu_appends_flag(value):
out = _run_ps1(value)
# The whole argv, not a substring: an `in` check passed while the harness was
# emitting --force-cpu twice. setup.ps1 appends it exactly once, and nothing
# else, for a cpu override.
assert out.strip() == "ARGS:--force-cpu"
@_SKIP_NO_PWSH
@pytest.mark.parametrize("value", [None, "", "auto", "AUTO", " "])
def test_ps1_backend_auto_no_flag_no_warn(value):
out = _run_ps1(value)
assert "--force-cpu" not in out
assert "Ignoring" not in out
@_SKIP_NO_PWSH
@pytest.mark.parametrize("value", ["vulkan", "VULKAN", " vulkan "])
def test_ps1_backend_vulkan_is_accepted(value):
out = _run_ps1(value)
assert "--force-cpu" not in out
assert "--llama-backend,vulkan" in out
assert "Ignoring" not in out
@_SKIP_NO_PWSH
@pytest.mark.parametrize("value", ["hip", "HIP", "rocm", " ROCM "])
def test_ps1_backend_hip_opt_out_is_accepted(value):
out = _run_ps1(value)
assert "--force-cpu" not in out
assert "--llama-backend" not in out
assert "Ignoring" not in out
def test_ps1_forced_vulkan_fails_closed_on_windows_arm64():
ps1 = _SETUP_PS1.read_text(encoding = "utf-8")
arm64_guard = ps1.index("elseif ($windowsArm64)")
vulkan_flag = ps1.index(
'$prebuiltArgs += @("--llama-backend", "vulkan")',
arm64_guard,
)
assert arm64_guard < vulkan_flag
assert "no Windows ARM64 Vulkan bundle is published" in ps1
assert "Unset UNSLOTH_FORCE_VULKAN" in ps1
@_SKIP_NO_PWSH
@pytest.mark.parametrize("value", ["gpu", "cuda"])
def test_ps1_backend_unknown_warns_and_no_flag(value):
out = _run_ps1(value)
assert "--force-cpu" not in out
assert "Ignoring" in out