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
40 changes: 23 additions & 17 deletions eng/tools/azure-sdk-tools/azpysdk/apistub.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,27 @@ def register(
dest="install_deps",
default=False,
action="store_true",
help=(
"Install dev requirements and apiview dependencies before running. "
"Skipped by default for faster local iteration."
),
help="Install target package dev requirements before running.",
)
p.set_defaults(func=self.run)

def ensure_apistub_dependencies(self, executable: str, package_dir: str, staging_directory: str) -> None:
try:
self.run_venv_command(executable, ["-c", "import apistub"], cwd=staging_directory, check=True)
return
except CalledProcessError:
logger.info("apistub module is not installed. Installing APIView dependencies.")

install_into_venv(
executable,
[
"-r",
os.path.join(REPO_ROOT, "eng", "apiview_reqs.txt"),
"--index-url=https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/",
],
package_dir,
)

def run(self, args: argparse.Namespace) -> int:
"""Run the apistub check command."""
logger.info("Running apistub check...")
Expand Down Expand Up @@ -121,19 +135,11 @@ def run(self, args: argparse.Namespace) -> int:
# install dependencies
self.install_dev_reqs(executable, args, package_dir)

try:
install_into_venv(
executable,
[
"-r",
os.path.join(REPO_ROOT, "eng", "apiview_reqs.txt"),
"--index-url=https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/",
],
package_dir,
)
except CalledProcessError as e:
logger.error(f"Failed to install dependencies: {e}")
return e.returncode
try:
self.ensure_apistub_dependencies(executable, package_dir, staging_directory)
except (CalledProcessError, RuntimeError) as e:
logger.error(f"Failed to install APIView dependencies: {e}")
return getattr(e, "returncode", 1)

if not os.getenv("PREBUILT_WHEEL_DIR"):
create_package_and_install(
Expand Down
71 changes: 67 additions & 4 deletions eng/tools/azure-sdk-tools/tests/test_apistub.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import sys
import pytest

from subprocess import CalledProcessError
from unittest.mock import patch, MagicMock

from azpysdk.apistub import apistub, get_package_wheel_path, get_cross_language_mapping_path


# ── get_package_wheel_path() ─────────────────────────────────────────────


Expand Down Expand Up @@ -128,7 +128,7 @@ def test_isolate_does_not_install_dependencies(
def test_install_deps_installs_dependencies(
self, _env, install_into_venv, _create, _get_whl, _get_mapping, tmp_path, monkeypatch
):
"""When --install-deps is passed, apistub should install dependencies."""
"""When --install-deps is passed, apistub should install target package dev requirements."""
monkeypatch.chdir(os.getcwd())
stub = apistub()
staging = str(tmp_path / "staging")
Expand All @@ -148,13 +148,68 @@ def test_install_deps_installs_dependencies(
stub.run(args)

install_dev_reqs.assert_called_once_with(sys.executable, args, str(tmp_path))
install_into_venv.assert_called_once()
install_into_venv.assert_not_called()
pip_freeze.assert_called_once_with(sys.executable)

@patch("azpysdk.apistub.logger.error")
@patch("azpysdk.apistub.set_envvar_defaults")
def test_runtime_error_installing_apiview_dependencies_returns_one(self, _env, logger_error, tmp_path, monkeypatch):
"""When APIView dependency install raises RuntimeError, run() should log and return 1."""
monkeypatch.chdir(os.getcwd())
stub = apistub()
staging = str(tmp_path / "staging")
os.makedirs(staging, exist_ok=True)
fake_parsed = MagicMock()
fake_parsed.folder = str(tmp_path)
fake_parsed.name = "azure-core"

with patch.object(stub, "get_targeted_directories", return_value=[fake_parsed]), patch.object(
stub, "get_executable", return_value=(sys.executable, staging)
), patch.object(stub, "ensure_apistub_dependencies", side_effect=RuntimeError("401 auth error")):
result = stub.run(self._make_args())

assert result == 1
logger_error.assert_called_once_with("Failed to install APIView dependencies: 401 auth error")

@patch(
"azpysdk.apistub.REPO_ROOT", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
)
@patch("azpysdk.apistub.install_into_venv")
def test_apistub_dependencies_are_skipped_when_installed(self, install_into_venv, tmp_path):
"""When apistub is already importable, APIView requirements should not be reinstalled."""
stub = apistub()

with patch.object(stub, "run_venv_command") as run_venv_command:
stub.ensure_apistub_dependencies(sys.executable, str(tmp_path), str(tmp_path))

run_venv_command.assert_called_once_with(
sys.executable, ["-c", "import apistub"], cwd=str(tmp_path), check=True
)
install_into_venv.assert_not_called()

@patch(
"azpysdk.apistub.REPO_ROOT", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
)
@patch("azpysdk.apistub.install_into_venv")
def test_missing_apistub_installs_apiview_requirements(self, install_into_venv, tmp_path):
"""When apistub is missing, APIView requirements should be installed once."""
stub = apistub()
repo_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))

with patch.object(
stub,
"run_venv_command",
side_effect=CalledProcessError(1, [sys.executable, "-c", "import apistub"]),
):
stub.ensure_apistub_dependencies(sys.executable, str(tmp_path), str(tmp_path))

install_into_venv.assert_called_once()
assert install_into_venv.call_args.args[0] == sys.executable
assert install_into_venv.call_args.args[1][0:2] == [
"-r",
os.path.join(repo_root, "eng", "apiview_reqs.txt"),
]
pip_freeze.assert_called_once_with(sys.executable)
assert install_into_venv.call_args.args[2] == str(tmp_path)

@patch(
"azpysdk.apistub.REPO_ROOT", os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", ".."))
Expand Down Expand Up @@ -196,6 +251,8 @@ def fake_pwsh(cmd, **kwargs):
with patch.object(stub, "get_targeted_directories", return_value=[fake_parsed]), patch.object(
stub, "get_executable", return_value=(sys.executable, staging)
), patch.object(stub, "install_dev_reqs"), patch.object(stub, "pip_freeze"), patch.object(
stub, "ensure_apistub_dependencies"
), patch.object(
stub, "run_venv_command", side_effect=fake_apistub_run
), patch(
"azpysdk.apistub.run", side_effect=fake_pwsh
Expand Down Expand Up @@ -244,6 +301,8 @@ def fake_pwsh(cmd, **kwargs):
with patch.object(stub, "get_targeted_directories", return_value=[fake_parsed]), patch.object(
stub, "get_executable", return_value=(sys.executable, staging)
), patch.object(stub, "install_dev_reqs"), patch.object(stub, "pip_freeze"), patch.object(
stub, "ensure_apistub_dependencies"
), patch.object(
stub, "run_venv_command", side_effect=fake_apistub_run
), patch(
"azpysdk.apistub.run", side_effect=fake_pwsh
Expand Down Expand Up @@ -293,6 +352,8 @@ def fake_pwsh(cmd, **kwargs):
with patch.object(stub, "get_targeted_directories", return_value=[fake_parsed]), patch.object(
stub, "get_executable", return_value=(sys.executable, staging)
), patch.object(stub, "install_dev_reqs"), patch.object(stub, "pip_freeze"), patch.object(
stub, "ensure_apistub_dependencies"
), patch.object(
stub, "run_venv_command", side_effect=fake_apistub_run
), patch(
"azpysdk.apistub.run", side_effect=fake_pwsh
Expand Down Expand Up @@ -330,6 +391,8 @@ def fake_apistub_run(exe, cmds, **kwargs):
with patch.object(stub, "get_targeted_directories", return_value=[fake_parsed]), patch.object(
stub, "get_executable", return_value=(sys.executable, staging)
), patch.object(stub, "install_dev_reqs"), patch.object(stub, "pip_freeze"), patch.object(
stub, "ensure_apistub_dependencies"
), patch.object(
stub, "run_venv_command", side_effect=fake_apistub_run
):
stub.run(self._make_args(generate_md=False))
Expand Down
Loading