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
24 changes: 13 additions & 11 deletions cpp_linter_hooks/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import logging
from typing import Optional, List

try:
if sys.version_info >= (3, 11):
import tomllib
except ModuleNotFoundError:
else:
import tomli as tomllib

from cpp_linter_hooks.versions import CLANG_FORMAT_VERSIONS, CLANG_TIDY_VERSIONS
Expand Down Expand Up @@ -60,16 +60,18 @@ def parse_version(v: str):


def _install_tool(tool: str, version: str) -> Optional[Path]:
"""Install a tool using pip, suppressing output."""
try:
subprocess.check_call(
[sys.executable, "-m", "pip", "install", f"{tool}=={version}"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
"""Install a tool using pip, logging output on failure."""
result = subprocess.run(
[sys.executable, "-m", "pip", "install", f"{tool}=={version}"],
capture_output=True,
text=True,
)
if result.returncode == 0:
return shutil.which(tool)
except subprocess.CalledProcessError:
return None
LOG.error("pip failed to install %s %s", tool, version)
LOG.error(result.stdout)
LOG.error(result.stderr)
return None


def resolve_install(tool: str, version: Optional[str]) -> Optional[Path]:
Expand Down
31 changes: 22 additions & 9 deletions tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,28 +123,34 @@ def test_install_tool_success():
"""Test _install_tool successful installation."""
mock_path = "/usr/bin/clang-format"

def patched_run(*args, **kwargs):
return subprocess.CompletedProcess(args, returncode=0)

with (
patch("subprocess.check_call") as mock_check_call,
patch("subprocess.run", side_effect=patched_run) as mock_run,
patch("shutil.which", return_value=mock_path),
):
result = _install_tool("clang-format", "20.1.7")
assert result == mock_path

mock_check_call.assert_called_once_with(
mock_run.assert_called_once_with(
[sys.executable, "-m", "pip", "install", "clang-format==20.1.7"],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
capture_output=True,
text=True,
)


@pytest.mark.benchmark
def test_install_tool_failure():
"""Test _install_tool when pip install fails."""

def patched_run(*args, **kwargs):
return subprocess.CompletedProcess(
args, returncode=1, stderr="Error", stdout="Installation failed"
)

with (
patch(
"subprocess.check_call",
side_effect=subprocess.CalledProcessError(1, ["pip"]),
),
patch("subprocess.run", side_effect=patched_run),
patch("cpp_linter_hooks.util.LOG"),
):
result = _install_tool("clang-format", "20.1.7")
Expand All @@ -154,7 +160,14 @@ def test_install_tool_failure():
@pytest.mark.benchmark
def test_install_tool_success_but_not_found():
"""Test _install_tool when install succeeds but tool not found in PATH."""
with patch("subprocess.check_call"), patch("shutil.which", return_value=None):

def patched_run(*args, **kwargs):
return subprocess.CompletedProcess(args, returncode=0)

with (
patch("subprocess.run", side_effect=patched_run),
patch("shutil.which", return_value=None),
):
result = _install_tool("clang-format", "20.1.7")
assert result is None

Expand Down
Loading