CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
196 lines
6.4 KiB
Python
196 lines
6.4 KiB
Python
"""Tests for perfxpert.tools._tooldep — external-tool verifier."""
|
|
|
|
import os
|
|
import subprocess
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
from perfxpert.tools import _tooldep
|
|
from perfxpert.tools._tooldep import (
|
|
ExternalToolMissing,
|
|
check_tool_available,
|
|
require_tool,
|
|
offer_install,
|
|
)
|
|
|
|
# -- registry sanity --------------------------------------------------------
|
|
|
|
|
|
def test_registry_contains_expected_externals():
|
|
"""The registry must enumerate every external dep used by the codebase."""
|
|
required = {
|
|
"opencode",
|
|
"rocprofv3",
|
|
"amdclang++",
|
|
"rocprof-trace-decoder", # ATT library
|
|
"mcp", # python
|
|
"pexpect",
|
|
}
|
|
registered = set(_tooldep._TOOL_REGISTRY.keys())
|
|
missing = required - registered
|
|
assert not missing, f"missing from registry: {missing}"
|
|
|
|
|
|
# -- check_tool_available ---------------------------------------------------
|
|
|
|
|
|
def test_check_binary_present_returns_true(monkeypatch, tmp_path):
|
|
fake = tmp_path / "some-binary"
|
|
fake.write_text("#!/bin/sh\n")
|
|
fake.chmod(0o755)
|
|
monkeypatch.setattr(_tooldep.shutil, "which", lambda name: str(fake))
|
|
# Stub subprocess.run so the smoke-test always succeeds
|
|
monkeypatch.setattr(
|
|
_tooldep.subprocess,
|
|
"run",
|
|
lambda cmd, **kw: mock.MagicMock(returncode=0),
|
|
)
|
|
ok, detail = check_tool_available("opencode")
|
|
assert ok is True
|
|
assert str(fake) in detail
|
|
|
|
|
|
def test_check_binary_smoke_test_oserror_returns_false(monkeypatch, tmp_path):
|
|
"""Binary found on PATH but OSError during smoke-test → (False, msg)."""
|
|
fake = tmp_path / "broken-binary"
|
|
fake.write_text("#!/bin/sh\n")
|
|
fake.chmod(0o755)
|
|
monkeypatch.setattr(_tooldep.shutil, "which", lambda name: str(fake))
|
|
|
|
def _raise_oserror(cmd, **kw):
|
|
raise OSError("exec format error")
|
|
|
|
monkeypatch.setattr(_tooldep.subprocess, "run", _raise_oserror)
|
|
ok, detail = check_tool_available("opencode")
|
|
assert ok is False
|
|
assert "smoke-test failed" in detail
|
|
|
|
|
|
def test_check_binary_smoke_test_nonzero_returns_false(monkeypatch, tmp_path):
|
|
"""Binary found on PATH but smoke-test exits non-zero → (False, msg)."""
|
|
fake = tmp_path / "bad-opencode"
|
|
fake.write_text("#!/bin/sh\n")
|
|
fake.chmod(0o755)
|
|
monkeypatch.setattr(_tooldep.shutil, "which", lambda name: str(fake))
|
|
monkeypatch.setattr(
|
|
_tooldep.subprocess,
|
|
"run",
|
|
lambda cmd, **kw: mock.MagicMock(returncode=127),
|
|
)
|
|
ok, detail = check_tool_available("opencode")
|
|
assert ok is False
|
|
assert "exited 127" in detail
|
|
|
|
|
|
def test_check_binary_smoke_test_timeout_returns_false(monkeypatch, tmp_path):
|
|
"""Binary found on PATH but smoke-test times out → (False, msg)."""
|
|
fake = tmp_path / "hung-opencode"
|
|
fake.write_text("#!/bin/sh\n")
|
|
fake.chmod(0o755)
|
|
monkeypatch.setattr(_tooldep.shutil, "which", lambda name: str(fake))
|
|
|
|
def _raise_timeout(cmd, **kw):
|
|
raise subprocess.TimeoutExpired(cmd, 2)
|
|
|
|
monkeypatch.setattr(_tooldep.subprocess, "run", _raise_timeout)
|
|
ok, detail = check_tool_available("opencode")
|
|
assert ok is False
|
|
assert "timed out" in detail
|
|
|
|
|
|
def test_check_binary_missing_returns_false(monkeypatch):
|
|
monkeypatch.setattr(_tooldep.shutil, "which", lambda name: None)
|
|
ok, detail = check_tool_available("opencode")
|
|
assert ok is False
|
|
assert "install" in detail.lower() or "not found" in detail.lower()
|
|
|
|
|
|
def test_require_tool_accepts_explicit_binary_path_for_registered_dep(tmp_path):
|
|
fake_cxx = tmp_path / "amdclang++"
|
|
fake_cxx.write_text("#!/bin/sh\n")
|
|
fake_cxx.chmod(0o755)
|
|
|
|
detail = require_tool("amdclang++", executable=str(fake_cxx))
|
|
|
|
assert str(fake_cxx) in detail
|
|
|
|
|
|
def test_check_pylib_present_returns_true():
|
|
# mcp is a required runtime dep and should be importable
|
|
ok, detail = check_tool_available("mcp")
|
|
# Either mcp is installed OR the test env doesn't have it — both are OK.
|
|
assert isinstance(ok, bool)
|
|
|
|
|
|
def test_check_pylib_missing_returns_false(monkeypatch):
|
|
monkeypatch.setattr(_tooldep.importlib.util, "find_spec", lambda name: None)
|
|
ok, detail = check_tool_available("mcp")
|
|
assert ok is False
|
|
assert "pip install" in detail
|
|
|
|
|
|
# -- require_tool -----------------------------------------------------------
|
|
|
|
|
|
def test_require_tool_raises_when_missing(monkeypatch):
|
|
monkeypatch.setattr(_tooldep.shutil, "which", lambda name: None)
|
|
with pytest.raises(ExternalToolMissing) as exc:
|
|
require_tool("opencode")
|
|
assert "opencode" in str(exc.value)
|
|
assert exc.value.install_hint
|
|
# Install command exists for opencode (curl install script)
|
|
assert exc.value.install_cmd is not None
|
|
|
|
|
|
def test_require_tool_with_allow_install_offers(monkeypatch, capsys):
|
|
"""If PERFXPERT_ALLOW_INSTALL=1 the helper prompts the user."""
|
|
monkeypatch.setenv("PERFXPERT_ALLOW_INSTALL", "1")
|
|
monkeypatch.setattr(_tooldep.shutil, "which", lambda name: None)
|
|
# Simulate user pressing 'n'
|
|
monkeypatch.setattr("builtins.input", lambda _: "n")
|
|
with pytest.raises(ExternalToolMissing):
|
|
require_tool("opencode", allow_install=True)
|
|
captured = capsys.readouterr()
|
|
assert "install" in captured.out.lower()
|
|
|
|
|
|
# -- offer_install ----------------------------------------------------------
|
|
|
|
|
|
def test_offer_install_declined_returns_false(monkeypatch, capsys):
|
|
monkeypatch.setattr("builtins.input", lambda _: "n")
|
|
ok = offer_install("opencode")
|
|
assert ok is False
|
|
|
|
|
|
def test_offer_install_accepted_runs_command(monkeypatch, capsys):
|
|
monkeypatch.setattr("builtins.input", lambda _: "y")
|
|
run_calls = []
|
|
|
|
def fake_run(cmd, **kw):
|
|
run_calls.append(cmd)
|
|
return mock.MagicMock(returncode=0)
|
|
|
|
monkeypatch.setattr(_tooldep.subprocess, "run", fake_run)
|
|
ok = offer_install("opencode")
|
|
assert ok is True
|
|
assert run_calls, "install command should have been invoked"
|
|
|
|
|
|
# -- ATT decoder (shared_lib kind) -----------------------------------------
|
|
|
|
|
|
def test_att_decoder_check_searches_rocm_paths(monkeypatch, tmp_path):
|
|
# Simulate the library existing under /opt/rocm
|
|
fake_lib = tmp_path / "librocprof-trace-decoder.so"
|
|
fake_lib.write_bytes(b"fake\n")
|
|
monkeypatch.setenv("ROCM_PATH", str(tmp_path))
|
|
monkeypatch.setattr(
|
|
_tooldep,
|
|
"_shared_lib_search_paths",
|
|
lambda name: [str(tmp_path)],
|
|
)
|
|
ok, detail = check_tool_available("rocprof-trace-decoder")
|
|
assert ok is True
|