Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

installer: report more details on build failures #8479

Merged
merged 2 commits into from
Sep 29, 2023
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
25 changes: 22 additions & 3 deletions src/poetry/installation/chef.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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"), {}),
Expand All @@ -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:
Expand Down
23 changes: 17 additions & 6 deletions src/poetry/installation/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 = (
"<info>"
"Note: This error originates from the build backend,"
" and is likely not a problem with poetry"
Expand All @@ -334,19 +334,30 @@ def _execute_operation(self, operation: Operation) -> None:
f" running '{pip_command} \"{requirement}\"'."
"</info>"
)
elif isinstance(e, ChefInstallError):
message = (
"<error>"
"Cannot install build-system.requires"
f" for {pkg.pretty_name}."
"</error>"
)
elif isinstance(e, SolverProblemError):
pkg = operation.package
io.write_line("")
io.write_line(
message = (
"<error>"
"Cannot resolve build-system.requires"
f" for {pkg.pretty_name}."
"</error>"
)
else:
message = f"<error>Cannot install {pkg.pretty_name}.</error>"

io.write_line("")
io.write_line(message)
io.write_line("")
finally:
with self._lock:
self._shutdown = True

except KeyboardInterrupt:
try:
message = (
Expand Down
43 changes: 43 additions & 0 deletions tests/installation/test_chef.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import os
import sys
import tempfile

from pathlib import Path
Expand All @@ -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
Expand All @@ -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,
Expand Down
102 changes: 97 additions & 5 deletions tests/installation/test_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)