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
6 changes: 6 additions & 0 deletions releasenotes/notes/wheel-path-enforce-baae211a3252a17a.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
fixes:
- |
``--wheel-path``/``RIOT_WHEEL_PATH`` now aborts the run if the wheel fails to
download or install, instead of silently falling back to an editable install.
This ensures that a specified wheel is always the one that gets used.
5 changes: 4 additions & 1 deletion riot/runner.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging
import os
from pathlib import Path
import shlex
import subprocess
import sys
import tempfile
Expand Down Expand Up @@ -102,7 +103,7 @@ def install_dev_pkg(
logger.warning("No Python setup file found. Skipping dev package installation.")
return

find_links_flag = f"--find-links {wheel_path}" if wheel_path else ""
find_links_flag = f"--find-links {shlex.quote(wheel_path)}" if wheel_path else ""

# Determine installation method
if wheel_path:
Expand All @@ -129,6 +130,7 @@ def install_dev_pkg(
wheel_path,
e.proc.stdout,
)
sys.exit(1)

# Step 2: Install the downloaded wheel
install_cmd = f"pip --disable-pip-version-check install {find_links_flag} '{tmp_dir}'/*.whl"
Expand All @@ -137,6 +139,7 @@ def install_dev_pkg(
dev_pkg_lockfile.touch()
except CmdFailure as e:
logger.error("Wheel installation failed!\n%s", e.proc.stdout)
sys.exit(1)

if not dev_pkg_lockfile.exists():
# Install in editable mode (legacy behavior)
Expand Down
24 changes: 19 additions & 5 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ def __call__(
) -> _T_CompletedProcess: ...


def _normalize_click_usage(text: str) -> str:
"""Normalize Click's usage line across Click versions.

Click >=8.2 (Python >=3.10) renders it as
``Usage: riot [OPTIONS] [COMMAND] [ARGS]...`` while older Click versions
(still resolved on Python 3.8/3.9, which Click >=8.2 no longer supports)
render it as ``Usage: riot [OPTIONS] COMMAND [ARGS]...``.
"""
return text.replace("[COMMAND]", "COMMAND")


@pytest.fixture
def tmp_run(tmp_path: pathlib.Path) -> Generator[_T_TmpRun, None, None]:
"""Run a command by default in tmp_path."""
Expand All @@ -64,7 +75,9 @@ def _run(

def test_no_riotfile(tmp_path: pathlib.Path, tmp_run: _T_TmpRun) -> None:
result = tmp_run("riot")
assert result.stderr == """Usage: riot [OPTIONS] COMMAND [ARGS]...
assert (
_normalize_click_usage(result.stderr)
== """Usage: riot [OPTIONS] COMMAND [ARGS]...

Options:
-f, --file PATH [default: riotfile.py]
Expand All @@ -83,11 +96,12 @@ def test_no_riotfile(tmp_path: pathlib.Path, tmp_run: _T_TmpRun) -> None:
run Run virtualenv instances with names matching a pattern.
shell Launch a shell inside a venv.
"""
)
assert result.stdout == ""
assert result.returncode == 2

result = tmp_run("riot -P list")
assert result.stderr == """
assert _normalize_click_usage(result.stderr) == """
Usage: riot [OPTIONS] COMMAND [ARGS]...
Try 'riot --help' for help.

Expand All @@ -99,7 +113,7 @@ def test_no_riotfile(tmp_path: pathlib.Path, tmp_run: _T_TmpRun) -> None:

def test_bad_riotfile(tmp_path: pathlib.Path, tmp_run: _T_TmpRun) -> None:
result = tmp_run("riot --file rf.py", tmp_path)
assert result.stderr == """
assert _normalize_click_usage(result.stderr) == """
Usage: riot [OPTIONS] COMMAND [ARGS]...
Try 'riot --help' for help.

Expand Down Expand Up @@ -141,7 +155,7 @@ def test_bad_riotfile(tmp_path: pathlib.Path, tmp_run: _T_TmpRun) -> None:

def test_help(tmp_run: _T_TmpRun) -> None:
result = tmp_run("riot --help")
assert result.stdout == """
assert _normalize_click_usage(result.stdout) == """
Usage: riot [OPTIONS] COMMAND [ARGS]...

Options:
Expand Down Expand Up @@ -174,7 +188,7 @@ def test_version(tmp_run: _T_TmpRun) -> None:

def test_list_no_file_empty_file(tmp_path: pathlib.Path, tmp_run: _T_TmpRun) -> None:
result = tmp_run("riot -P list")
assert result.stderr == """
assert _normalize_click_usage(result.stderr) == """
Usage: riot [OPTIONS] COMMAND [ARGS]...
Try 'riot --help' for help.

Expand Down
88 changes: 88 additions & 0 deletions tests/test_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,94 @@ def test_wheel_path_cli_option_passes_through(monkeypatch, tmp_path):
os.chdir(old_cwd)


def _make_cmd_failure():
from riot.exceptions import CmdFailure

proc = subprocess.CompletedProcess(args=["pip"], returncode=1, stdout="boom")
return CmdFailure("cmd failed", proc)


def test_install_dev_pkg_exits_when_wheel_download_fails(monkeypatch, tmp_path):
"""A failed wheel download must abort, never fall back to an editable install."""
from unittest.mock import patch

from riot.runner import install_dev_pkg

old_cwd = os.getcwd()
try:
os.chdir(tmp_path)
pathlib.Path("pyproject.toml").write_text('[project]\nname = "test-pkg"')

with patch(
"riot.runner.run_cmd_venv", side_effect=_make_cmd_failure()
) as mock_run:
with pytest.raises(SystemExit) as exc_info:
install_dev_pkg(
str(tmp_path / "venv"), force=True, wheel_path="/tmp/wheels"
)

assert exc_info.value.code == 1
# Only the download step should have been attempted -- no
# fallback `pip install -e .` call.
assert mock_run.call_count == 1
assert "download" in mock_run.call_args.args[1]
finally:
os.chdir(old_cwd)


def test_install_dev_pkg_exits_when_wheel_install_fails(monkeypatch, tmp_path):
"""A failed wheel install must abort, never fall back to an editable install."""
from unittest.mock import patch

from riot.runner import install_dev_pkg

old_cwd = os.getcwd()
try:
os.chdir(tmp_path)
pathlib.Path("pyproject.toml").write_text('[project]\nname = "test-pkg"')

# First call (download) succeeds, second call (install) fails.
with patch(
"riot.runner.run_cmd_venv", side_effect=[None, _make_cmd_failure()]
) as mock_run:
with pytest.raises(SystemExit) as exc_info:
install_dev_pkg(
str(tmp_path / "venv"), force=True, wheel_path="/tmp/wheels"
)

assert exc_info.value.code == 1
assert mock_run.call_count == 2
assert "install" in mock_run.call_args.args[1]
finally:
os.chdir(old_cwd)


def test_install_dev_pkg_wheel_success_skips_editable_fallback(monkeypatch, tmp_path):
"""A successful wheel install must not also attempt an editable install."""
from unittest.mock import patch

from riot.runner import install_dev_pkg

old_cwd = os.getcwd()
try:
os.chdir(tmp_path)
pathlib.Path("pyproject.toml").write_text('[project]\nname = "test-pkg"')
venv_path = tmp_path / "venv"
venv_path.mkdir()

with patch("riot.runner.run_cmd_venv") as mock_run:
install_dev_pkg(str(venv_path), force=True, wheel_path="/tmp/wheels")

assert mock_run.call_count == 2
download_cmd, install_cmd = (c.args[1] for c in mock_run.call_args_list)
assert "download" in download_cmd
assert "install" in install_cmd
assert "-e ." not in install_cmd
assert (venv_path / ".riot-dev-pkg-installed").exists()
finally:
os.chdir(old_cwd)


def test_requirements_upgrades_compatible_pip_tools(
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
) -> None:
Expand Down
Loading