Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 177 additions & 2 deletions tests/test_install.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
from __future__ import annotations

from unittest.mock import patch

import pytest

from tools.install import SystemInfo, plan_install
from tools.install import SystemInfo, _auto_accelerator, _resolve_accelerator, plan_install


def _step_args(system: SystemInfo, **kwargs: object) -> list[tuple[str, ...]]:
return [step.args for step in plan_install(system, **kwargs).steps]


# ---------------------------------------------------------------------------
# CI safety
# ---------------------------------------------------------------------------


def test_ci_install_is_hardware_independent() -> None:
system = SystemInfo("Linux", "x86_64", ("NVIDIA RTX 4090",))

Expand All @@ -26,6 +33,11 @@ def test_ci_install_is_hardware_independent() -> None:
assert any("hardware-independent" in note for note in plan.notes)


# ---------------------------------------------------------------------------
# Windows auto-select
# ---------------------------------------------------------------------------


def test_windows_auto_uses_directml_swap() -> None:
system = SystemInfo("Windows", "AMD64", ("AMD Radeon",))

Expand All @@ -43,6 +55,40 @@ def test_windows_auto_uses_directml_swap() -> None:
assert ("install", "-r", "requirements/gpu-directml.txt") in args


def test_windows_intel_arc_auto_uses_openvino() -> None:
"""Arc GPU on Windows should get OpenVINO, not DirectML."""
system = SystemInfo("Windows", "AMD64", ("Intel Arc A770",))

args = _step_args(system)

assert ("install", "-r", "requirements/openvino.txt") in args
assert ("install", "-r", "requirements/gpu-directml.txt") not in args


def test_windows_intel_iris_auto_uses_openvino() -> None:
system = SystemInfo("Windows", "AMD64", ("Intel Iris Xe Graphics",))

assert _auto_accelerator(system) == "openvino"


def test_windows_intel_uhd_auto_uses_openvino() -> None:
system = SystemInfo("Windows", "AMD64", ("Intel UHD Graphics 770",))

assert _auto_accelerator(system) == "openvino"


def test_windows_nvidia_beats_intel_igpu() -> None:
"""NVIDIA dGPU + Intel iGPU on Windows → nvidia (not openvino, not directml)."""
system = SystemInfo("Windows", "AMD64", ("Intel UHD Graphics 770", "NVIDIA RTX 4090"))

assert _auto_accelerator(system) == "nvidia"


# ---------------------------------------------------------------------------
# Linux auto-select
# ---------------------------------------------------------------------------


def test_linux_nvidia_auto_uses_nvidia_swap() -> None:
system = SystemInfo("Linux", "x86_64", ("NVIDIA RTX 4090",))

Expand All @@ -61,7 +107,7 @@ def test_linux_nvidia_auto_uses_nvidia_swap() -> None:
assert ("install", "-r", "requirements/gpu-nvidia.txt") in args


def test_linux_intel_auto_uses_openvino_swap() -> None:
def test_linux_intel_uhd_auto_uses_openvino_swap() -> None:
# Intel CPU/iGPU on Linux with no NVIDIA → OpenVINO (faster than the stock CPU EP).
system = SystemInfo("Linux", "x86_64", ("Intel Corporation UHD Graphics 770",))

Expand All @@ -70,6 +116,46 @@ def test_linux_intel_auto_uses_openvino_swap() -> None:
assert ("install", "-r", "requirements/openvino.txt") in args


def test_linux_intel_arc_auto_uses_openvino() -> None:
"""Arc GPU reported by lspci matches on 'arc' token."""
system = SystemInfo("Linux", "x86_64", ("Intel Corporation DG2 [Arc A770]",))

assert _auto_accelerator(system) == "openvino"


def test_linux_pci_8086_matches_intel_gpu() -> None:
"""lspci output containing '8086' vendor prefix is treated as Intel GPU."""
system = SystemInfo("Linux", "x86_64", ("00:02.0 VGA compatible controller [0300]: 8086:a780",))

assert system.has_intel_gpu is True


def test_linux_intel_cpu_only_auto_uses_openvino() -> None:
"""Intel CPU with no GPU listed → OpenVINO via /proc/cpuinfo probe."""
system = SystemInfo("Linux", "x86_64", ()) # no GPU name detected

with patch(
"tools.install._run_command_lines",
return_value=("model name : Intel(R) Core(TM) i7-1165G7 @ 2.80GHz",),
):
result = _auto_accelerator(system)

assert result == "openvino"


def test_linux_amd_cpu_no_gpu_falls_through_to_none() -> None:
"""AMD/unknown CPU with no GPU → None (base CPU EP)."""
system = SystemInfo("Linux", "x86_64", ())

with patch(
"tools.install._run_command_lines",
return_value=("model name : AMD Ryzen 9 5950X",),
):
result = _auto_accelerator(system)

assert result is None


def test_linux_nvidia_beats_intel_when_both_present() -> None:
# A box with both an NVIDIA dGPU and an Intel iGPU should pick NVIDIA, not OpenVINO.
system = SystemInfo("Linux", "x86_64", ("Intel UHD Graphics", "NVIDIA RTX 4090"))
Expand All @@ -80,6 +166,11 @@ def test_linux_nvidia_beats_intel_when_both_present() -> None:
assert ("install", "-r", "requirements/openvino.txt") not in args


# ---------------------------------------------------------------------------
# macOS auto-select and openvino gate
# ---------------------------------------------------------------------------


def test_macos_auto_stays_on_base_coreml_wheel() -> None:
system = SystemInfo("Darwin", "arm64", ("Apple M3",))

Expand All @@ -90,6 +181,67 @@ def test_macos_auto_stays_on_base_coreml_wheel() -> None:
assert any("CoreML" in note for note in plan.notes)


def test_macos_openvino_explicit_raises_clear_error() -> None:
"""--accelerator openvino on macOS must fail with an actionable message."""
system = SystemInfo("Darwin", "arm64", ("Apple M3",))

with pytest.raises(ValueError, match="no macOS wheel"):
_resolve_accelerator(system, "openvino")


def test_macos_openvino_via_plan_install_raises() -> None:
"""plan_install propagates the ValueError so main() can surface it."""
system = SystemInfo("Darwin", "arm64", ("Apple M3",))

with pytest.raises(ValueError, match="no macOS wheel"):
plan_install(system, accelerator="openvino")


def test_macos_intel_igpu_auto_stays_base() -> None:
"""Even if a macOS machine has an Intel GPU label, no openvino wheel exists."""
system = SystemInfo("Darwin", "x86_64", ("Intel Iris Plus Graphics",))

assert _auto_accelerator(system) is None


# ---------------------------------------------------------------------------
# has_intel_gpu detection coverage
# ---------------------------------------------------------------------------


def test_has_intel_gpu_matches_arc() -> None:
assert SystemInfo("Linux", "x86_64", ("Arc A770",)).has_intel_gpu is True


def test_has_intel_gpu_matches_iris() -> None:
assert SystemInfo("Linux", "x86_64", ("Iris Xe Graphics",)).has_intel_gpu is True


def test_has_intel_gpu_matches_uhd() -> None:
assert SystemInfo("Linux", "x86_64", ("UHD Graphics 770",)).has_intel_gpu is True


def test_has_intel_gpu_matches_intel_substring() -> None:
assert SystemInfo("Linux", "x86_64", ("Intel Corporation HD Graphics",)).has_intel_gpu is True


def test_has_intel_gpu_rejects_amd() -> None:
assert SystemInfo("Linux", "x86_64", ("AMD Radeon RX 7900 XTX",)).has_intel_gpu is False


def test_has_intel_gpu_rejects_nvidia() -> None:
assert SystemInfo("Linux", "x86_64", ("NVIDIA GeForce RTX 4090",)).has_intel_gpu is False


def test_has_intel_gpu_rejects_empty() -> None:
assert SystemInfo("Linux", "x86_64", ()).has_intel_gpu is False


# ---------------------------------------------------------------------------
# Other platform guards
# ---------------------------------------------------------------------------


def test_ui_only_skips_accelerator_profiles() -> None:
system = SystemInfo("Windows", "AMD64", ("NVIDIA RTX 4090",))

Expand All @@ -104,3 +256,26 @@ def test_directml_is_windows_only() -> None:

with pytest.raises(ValueError, match="DirectML"):
plan_install(system, accelerator="directml")


def test_nvidia_is_linux_or_windows_only() -> None:
system = SystemInfo("Darwin", "arm64")

with pytest.raises(ValueError, match="NVIDIA"):
plan_install(system, accelerator="nvidia")


def test_openvino_explicit_on_linux_works() -> None:
system = SystemInfo("Linux", "x86_64", ())

args = _step_args(system, accelerator="openvino")

assert ("install", "-r", "requirements/openvino.txt") in args


def test_openvino_explicit_on_windows_works() -> None:
system = SystemInfo("Windows", "AMD64", ())

args = _step_args(system, accelerator="openvino")

assert ("install", "-r", "requirements/openvino.txt") in args
58 changes: 51 additions & 7 deletions tools/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,25 @@ def has_nvidia_gpu(self) -> bool:

@property
def has_intel_gpu(self) -> bool:
return any("intel" in name.lower() for name in self.gpu_names)
"""True when any detected GPU is clearly Intel.

Matches canonical Intel GPU brand strings (Arc, Iris, UHD) *and* the
PCI vendor string "intel" so that lspci lines like "Intel Corporation
DG2 [Arc A770]" or "8086:" prefix entries are also caught. A bare
substring "intel" in the adapter name is sufficient; vendor ID ``8086``
or the word "arc"/"iris"/"uhd" are additional signals.
"""
_INTEL_TOKENS = ("intel", "arc", "iris", "uhd", "8086")
return any(any(tok in name.lower() for tok in _INTEL_TOKENS) for name in self.gpu_names)

@property
def has_discrete_non_intel_gpu(self) -> bool:
"""True when any detected GPU is a discrete NVIDIA or AMD card."""
return self.has_nvidia_gpu or any(
"amd" in name.lower() or "radeon" in name.lower()
for name in self.gpu_names
if "intel" not in name.lower()
)

def label(self) -> str:
gpu_label = ", ".join(self.gpu_names) if self.gpu_names else "no GPU name detected"
Expand Down Expand Up @@ -177,14 +195,35 @@ def detect_system() -> SystemInfo:


def _auto_accelerator(system: SystemInfo) -> Accelerator | None:
"""Choose the safest useful accelerator for a normal local install."""
if system.is_windows:
return "directml"
if system.is_linux and system.has_nvidia_gpu:
"""Choose the safest useful accelerator for a normal local install.

Selection priority (first match wins):
1. macOS → None (base wheel; CoreML picked up automatically)
2. NVIDIA present → nvidia (Windows or Linux)
3. Intel GPU → openvino (Arc / Iris / UHD on Windows or Linux)
4. Intel CPU only → openvino (beats the stock CPU EP on all Intel boxes)
5. Windows AMD → directml (DX12 fallback; covers AMD/other dGPUs)
6. Everything else → None (base CPU EP)

OpenVINO is never auto-selected on macOS because there is no macOS wheel.
The ``--ci`` path forces ``accelerator="cpu"`` before this function is
called, so CI always lands on the None/base branch.
"""
if system.is_macos:
return None
if system.has_nvidia_gpu:
return "nvidia"
# Intel CPU/iGPU (no discrete NVIDIA): OpenVINO beats the stock CPU EP on Intel.
if system.is_linux and system.has_intel_gpu:
# Intel GPU (Arc / Iris / UHD) on Windows or Linux → OpenVINO.
if system.has_intel_gpu:
return "openvino"
# Linux Intel CPU with no discrete GPU → OpenVINO beats the stock CPU EP.
# (Windows CPU-only boxes already get DirectML below, which is the existing behaviour.)
if system.is_linux and not system.has_discrete_non_intel_gpu:
cpu_info = _run_command_lines(("grep", "-m1", "model name", "/proc/cpuinfo"))
if any("intel" in line.lower() for line in cpu_info):
return "openvino"
if system.is_windows:
return "directml"
return None


Expand All @@ -203,6 +242,11 @@ def _resolve_accelerator(system: SystemInfo, requested: Accelerator) -> Accelera
raise ValueError("The NVIDIA ONNX Runtime wheel is only for Windows/Linux.")
return "nvidia"
if requested == "openvino":
if system.is_macos:
raise ValueError(
"OpenVINO has no macOS wheel; use the default CoreML/CPU build"
" (omit --accelerator or pass --accelerator cpu)."
)
return "openvino"
raise ValueError(f"Unknown accelerator: {requested}")

Expand Down
Loading