From 081146956349e24d901c5187685df345e6621a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Randy=20D=C3=B6ring?= <30527984+radoering@users.noreply.github.com> Date: Fri, 29 Sep 2023 11:07:38 +0200 Subject: [PATCH] installer: report more details on build failures (#8479) --- src/poetry/installation/chef.py | 25 ++++++- src/poetry/installation/executor.py | 23 +++++-- tests/installation/test_chef.py | 43 ++++++++++++ tests/installation/test_executor.py | 102 ++++++++++++++++++++++++++-- 4 files changed, 179 insertions(+), 14 deletions(-) diff --git a/src/poetry/installation/chef.py b/src/poetry/installation/chef.py index 18e99ae49bf..d734d0dcf83 100644 --- a/src/poetry/installation/chef.py +++ b/src/poetry/installation/chef.py @@ -36,6 +36,23 @@ class ChefError(Exception): ... class ChefBuildError(ChefError): ... +class ChefInstallError(ChefError): + def __init__(self, requirements: Collection[str], output: str, error: str) -> None: + message = "\n\n".join( + ( + f"Failed to install {', '.join(requirements)}.", + f"Output:\n{output}", + f"Error:\n{error}", + ) + ) + super().__init__(message) + self._requirements = requirements + + @property + def requirements(self) -> Collection[str]: + return self._requirements + + class IsolatedEnv(BaseIsolatedEnv): def __init__(self, env: Env, pool: RepositoryPool) -> None: self._env = env @@ -57,7 +74,7 @@ def make_extra_environ(self) -> dict[str, str]: } def install(self, requirements: Collection[str]) -> None: - from cleo.io.null_io import NullIO + from cleo.io.buffered_io import BufferedIO from poetry.core.packages.dependency import Dependency from poetry.core.packages.project_package import ProjectPackage @@ -73,8 +90,9 @@ def install(self, requirements: Collection[str]) -> None: dependency = Dependency.create_from_pep_508(requirement) package.add_dependency(dependency) + io = BufferedIO() installer = Installer( - NullIO(), + io, self._env, package, Locker(self._env.path.joinpath("poetry.lock"), {}), @@ -83,7 +101,8 @@ def install(self, requirements: Collection[str]) -> None: InstalledRepository.load(self._env), ) installer.update(True) - installer.run() + if installer.run() != 0: + raise ChefInstallError(requirements, io.fetch_output(), io.fetch_error()) class Chef: diff --git a/src/poetry/installation/executor.py b/src/poetry/installation/executor.py index 171616d6d00..a0ebcbcf910 100644 --- a/src/poetry/installation/executor.py +++ b/src/poetry/installation/executor.py @@ -20,6 +20,7 @@ from poetry.installation.chef import Chef from poetry.installation.chef import ChefBuildError +from poetry.installation.chef import ChefInstallError from poetry.installation.chooser import Chooser from poetry.installation.operations import Install from poetry.installation.operations import Uninstall @@ -314,8 +315,8 @@ def _execute_operation(self, operation: Operation) -> None: with self._lock: trace = ExceptionTrace(e) trace.render(io) + pkg = operation.package if isinstance(e, ChefBuildError): - pkg = operation.package pip_command = "pip wheel --no-cache-dir --use-pep517" if pkg.develop: requirement = pkg.source_url @@ -324,8 +325,7 @@ def _execute_operation(self, operation: Operation) -> None: requirement = ( pkg.to_dependency().to_pep_508().split(";")[0].strip() ) - io.write_line("") - io.write_line( + message = ( "" "Note: This error originates from the build backend," " and is likely not a problem with poetry" @@ -334,19 +334,30 @@ def _execute_operation(self, operation: Operation) -> None: f" running '{pip_command} \"{requirement}\"'." "" ) + elif isinstance(e, ChefInstallError): + message = ( + "" + "Cannot install build-system.requires" + f" for {pkg.pretty_name}." + "" + ) elif isinstance(e, SolverProblemError): - pkg = operation.package - io.write_line("") - io.write_line( + message = ( "" "Cannot resolve build-system.requires" f" for {pkg.pretty_name}." "" ) + else: + message = f"Cannot install {pkg.pretty_name}." + + io.write_line("") + io.write_line(message) io.write_line("") finally: with self._lock: self._shutdown = True + except KeyboardInterrupt: try: message = ( diff --git a/tests/installation/test_chef.py b/tests/installation/test_chef.py index d134b90f9c0..fb26057a72e 100644 --- a/tests/installation/test_chef.py +++ b/tests/installation/test_chef.py @@ -1,6 +1,7 @@ from __future__ import annotations import os +import sys import tempfile from pathlib import Path @@ -13,12 +14,19 @@ from poetry.factory import Factory from poetry.installation.chef import Chef +from poetry.installation.chef import ChefInstallError +from poetry.installation.chef import IsolatedEnv +from poetry.puzzle.exceptions import SolverProblemError +from poetry.puzzle.provider import IncompatibleConstraintsError from poetry.repositories import RepositoryPool from poetry.utils.env import EnvManager +from poetry.utils.env import ephemeral_environment from tests.repositories.test_pypi_repository import MockRepository if TYPE_CHECKING: + from collections.abc import Collection + from pytest_mock import MockerFixture from poetry.utils.cache import ArtifactCache @@ -40,6 +48,41 @@ def setup(mocker: MockerFixture, pool: RepositoryPool) -> None: mocker.patch.object(Factory, "create_pool", return_value=pool) +def test_isolated_env_install_success(pool: RepositoryPool) -> None: + with ephemeral_environment(Path(sys.executable)) as venv: + env = IsolatedEnv(venv, pool) + assert "poetry-core" not in venv.run("pip", "freeze") + env.install({"poetry-core"}) + assert "poetry-core" in venv.run("pip", "freeze") + + +@pytest.mark.parametrize( + ("requirements", "exception"), + [ + ({"poetry-core==1.5.0", "poetry-core==1.6.0"}, IncompatibleConstraintsError), + ({"black==19.10b0", "attrs==17.4.0"}, SolverProblemError), + ], +) +def test_isolated_env_install_error( + requirements: Collection[str], exception: type[Exception], pool: RepositoryPool +) -> None: + with ephemeral_environment(Path(sys.executable)) as venv: + env = IsolatedEnv(venv, pool) + with pytest.raises(exception): + env.install(requirements) + + +def test_isolated_env_install_failure( + pool: RepositoryPool, mocker: MockerFixture +) -> None: + mocker.patch("poetry.installation.installer.Installer.run", return_value=1) + with ephemeral_environment(Path(sys.executable)) as venv: + env = IsolatedEnv(venv, pool) + with pytest.raises(ChefInstallError) as e: + env.install({"a", "b>1"}) + assert e.value.requirements == {"a", "b>1"} + + def test_prepare_sdist( config: Config, config_cache_dir: Path, diff --git a/tests/installation/test_executor.py b/tests/installation/test_executor.py index 1f08861feb2..4e0ad9cb091 100644 --- a/tests/installation/test_executor.py +++ b/tests/installation/test_executor.py @@ -1378,11 +1378,7 @@ def test_build_system_requires_not_available( .as_posix(), ) - return_code = executor.execute( - [ - Install(directory_package), - ] - ) + return_code = executor.execute([Install(directory_package)]) assert return_code == 1 @@ -1402,3 +1398,99 @@ def test_build_system_requires_not_available( output = io.fetch_output().strip() assert output.startswith(expected_start) assert output.endswith(expected_end) + + +def test_build_system_requires_install_failure( + mocker: MockerFixture, + config: Config, + pool: RepositoryPool, + io: BufferedIO, + mock_file_downloads: None, + env: MockEnv, + fixture_dir: FixtureDirGetter, +) -> None: + mocker.patch("poetry.installation.installer.Installer.run", return_value=1) + mocker.patch("cleo.io.buffered_io.BufferedIO.fetch_output", return_value="output") + mocker.patch("cleo.io.buffered_io.BufferedIO.fetch_error", return_value="error") + io.set_verbosity(Verbosity.NORMAL) + + executor = Executor(env, pool, config, io) + + package_name = "simple-project" + package_version = "1.2.3" + directory_package = Package( + package_name, + package_version, + source_type="directory", + source_url=fixture_dir("simple_project").resolve().as_posix(), + ) + + return_code = executor.execute([Install(directory_package)]) + + assert return_code == 1 + + package_url = directory_package.source_url + expected_start = f"""\ +Package operations: 1 install, 0 updates, 0 removals + + • Installing {package_name} ({package_version} {package_url}) + + ChefInstallError + + Failed to install poetry-core>=1.1.0a7. + \ + + Output: + output + \ + + Error: + error + +""" + expected_end = "Cannot install build-system.requires for simple-project." + + mocker.stopall() # to get real output + output = io.fetch_output().strip() + assert output.startswith(expected_start) + assert output.endswith(expected_end) + + +def test_other_error( + config: Config, + pool: RepositoryPool, + io: BufferedIO, + mock_file_downloads: None, + env: MockEnv, + fixture_dir: FixtureDirGetter, +) -> None: + io.set_verbosity(Verbosity.NORMAL) + + executor = Executor(env, pool, config, io) + + package_name = "simple-project" + package_version = "1.2.3" + directory_package = Package( + package_name, + package_version, + source_type="directory", + source_url=fixture_dir("non-existing").resolve().as_posix(), + ) + + return_code = executor.execute([Install(directory_package)]) + + assert return_code == 1 + + package_url = directory_package.source_url + expected_start = f"""\ +Package operations: 1 install, 0 updates, 0 removals + + • Installing {package_name} ({package_version} {package_url}) + + FileNotFoundError +""" + expected_end = "Cannot install simple-project." + + output = io.fetch_output().strip() + assert output.startswith(expected_start) + assert output.endswith(expected_end)