diff --git a/tests/test_installer.py b/tests/test_installer.py index 72182b7..729d64f 100644 --- a/tests/test_installer.py +++ b/tests/test_installer.py @@ -1,11 +1,14 @@ # pylint: disable=missing-module-docstring, redefined-outer-name, unused-argument, wrong-import-order, unused-import import time +from unittest import mock +import pytest import tox.venv from poetry.factory import Factory from .fixtures import mock_poetry_factory from .fixtures import mock_venv +from tox_poetry_installer import _poetry from tox_poetry_installer import installer from tox_poetry_installer import utilities @@ -57,3 +60,28 @@ def test_parallelization(mock_venv, mock_poetry_factory): assert round(parallel * 5) == round(sequential) assert round(sequential) == len(set(to_install)) assert round(parallel * 5) == len(set(to_install)) + + +@pytest.mark.parametrize("num_threads", (0, 8)) +def test_propagates_exceptions_during_installation( + mock_venv, mock_poetry_factory, num_threads +): + # Assert that an exception which occurs during installation is properly raised. + # Regression test for https://github.com/enpaul/tox-poetry-installer/issues/86 + poetry = Factory().create_poetry(None) + packages: utilities.PackageMap = { + item.name: item for item in poetry.locker.locked_repository().packages + } + to_install = [packages["toml"]] + venv = tox.venv.VirtualEnv() + fake_exception = ValueError("my testing exception") + + with mock.patch.object( + _poetry, + "PipInstaller", + **{"return_value.install.side_effect": fake_exception}, + ): + with pytest.raises(ValueError) as exc_info: + installer.install(poetry, venv, to_install, num_threads) + + assert exc_info.value is fake_exception diff --git a/tox_poetry_installer/installer.py b/tox_poetry_installer/installer.py index 5e0eda4..a0d1085 100644 --- a/tox_poetry_installer/installer.py +++ b/tox_poetry_installer/installer.py @@ -71,11 +71,20 @@ def install( yield lambda func, arg: func(arg) with _optional_parallelize() as executor: + futures = [] for dependency in packages: if dependency not in installed: installed.add(dependency) logger.debug(f"Queuing {dependency}") - executor(logged_install, dependency) + future = executor(logged_install, dependency) + if future is not None: + futures.append(future) else: logger.debug(f"Skipping {dependency}, already installed") logger.debug("Waiting for installs to finish...") + + for future in concurrent.futures.as_completed(futures): + # Don't actually care about the return value, just waiting on the + # future to ensure any exceptions that were raised in the called + # function are propagated. + future.result()