diff --git a/changelog.d/2459.change.rst b/changelog.d/2459.change.rst new file mode 100644 index 0000000000..3b8d11a9ed --- /dev/null +++ b/changelog.d/2459.change.rst @@ -0,0 +1 @@ +Tests now run in parallel via pytest-xdist, completing in about half the time. Special thanks to :user:`webknjaz` for hard work implementing test isolation. To run without parallelization, disable the plugin with ``tox -- -p no:xdist``. diff --git a/pyproject.toml b/pyproject.toml index 0bc2a46f4f..4e80bdc1a7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,9 @@ addopts = "--flake8" [pytest.enabler.cov] addopts = "--cov" +[pytest.enabler.xdist] +addopts = "-n auto" + [tool.towncrier] package = "setuptools" package_dir = "setuptools" diff --git a/setup.cfg b/setup.cfg index 536ec70fab..36c7daeebc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,7 +47,7 @@ testing = pytest-black >= 0.3.7; python_implementation != "PyPy" pytest-cov pytest-mypy; python_implementation != "PyPy" - pytest-enabler + pytest-enabler >= 1.0.1 # local mock @@ -58,6 +58,7 @@ testing = paver pip>=19.1 # For proper file:// URLs support. jaraco.envs + pytest-xdist docs = # Keep these in sync with docs/requirements.txt diff --git a/setuptools/tests/fixtures.py b/setuptools/tests/fixtures.py index e8cb7f5237..d74b5f031a 100644 --- a/setuptools/tests/fixtures.py +++ b/setuptools/tests/fixtures.py @@ -1,3 +1,7 @@ +import contextlib +import sys +import shutil + import pytest from . import contexts @@ -21,3 +25,36 @@ def user_override(monkeypatch): def tmpdir_cwd(tmpdir): with tmpdir.as_cwd() as orig: yield orig + + +@pytest.fixture +def tmp_src(request, tmp_path): + """Make a copy of the source dir under `$tmp/src`. + + This fixture is useful whenever it's necessary to run `setup.py` + or `pip install` against the source directory when there's no + control over the number of simultaneous invocations. Such + concurrent runs create and delete directories with the same names + under the target directory and so they influence each other's runs + when they are not being executed sequentially. + """ + tmp_src_path = tmp_path / 'src' + shutil.copytree(request.config.rootdir, tmp_src_path) + return tmp_src_path + + +@pytest.fixture(autouse=True, scope="session") +def workaround_xdist_376(request): + """ + Workaround pytest-dev/pytest-xdist#376 + + ``pytest-xdist`` tends to inject '' into ``sys.path``, + which may break certain isolation expectations. + Remove the entry so the import + machinery behaves the same irrespective of xdist. + """ + if not request.config.pluginmanager.has_plugin('xdist'): + return + + with contextlib.suppress(ValueError): + sys.path.remove('') diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 6d3a997ee0..e117d8e629 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -11,7 +11,7 @@ class BuildBackendBase: - def __init__(self, cwd=None, env={}, backend_name='setuptools.build_meta'): + def __init__(self, cwd='.', env={}, backend_name='setuptools.build_meta'): self.cwd = cwd self.env = env self.backend_name = backend_name @@ -126,7 +126,7 @@ class TestBuildMetaBackend: backend_name = 'setuptools.build_meta' def get_build_backend(self): - return BuildBackend(cwd='.', backend_name=self.backend_name) + return BuildBackend(backend_name=self.backend_name) @pytest.fixture(params=defns) def build_backend(self, tmpdir, request): @@ -337,7 +337,7 @@ def test_build_sdist_builds_targz_even_if_zip_indicated(self, tmpdir_cwd): def test_build_sdist_relative_path_import(self, tmpdir_cwd): build_files(self._relative_path_import_files) build_backend = self.get_build_backend() - with pytest.raises(ImportError): + with pytest.raises(ImportError, match="^No module named 'hello'$"): build_backend.build_sdist("temp") @pytest.mark.parametrize('setup_literal, requirements', [ diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py index a53773df8c..0e89921c90 100644 --- a/setuptools/tests/test_distutils_adoption.py +++ b/setuptools/tests/test_distutils_adoption.py @@ -21,10 +21,10 @@ def run(self, cmd, *args, **kwargs): @pytest.fixture -def venv(tmpdir): +def venv(tmp_path, tmp_src): env = VirtualEnv() - env.root = path.Path(tmpdir) - env.req = os.getcwd() + env.root = path.Path(tmp_path / 'venv') + env.req = str(tmp_src) return env.create() diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index 5a942d84c5..d72dcbd0e5 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -43,11 +43,11 @@ def bare_virtualenv(): SOURCE_DIR = os.path.join(os.path.dirname(__file__), '../..') -def test_clean_env_install(bare_virtualenv): +def test_clean_env_install(bare_virtualenv, tmp_src): """ Check setuptools can be installed in a clean environment. """ - bare_virtualenv.run(['python', 'setup.py', 'install'], cd=SOURCE_DIR) + bare_virtualenv.run(['python', 'setup.py', 'install'], cd=tmp_src) def _get_pip_versions(): @@ -85,7 +85,7 @@ def _get_pip_versions(): @pytest.mark.parametrize('pip_version', _get_pip_versions()) -def test_pip_upgrade_from_source(pip_version, virtualenv): +def test_pip_upgrade_from_source(pip_version, tmp_src, virtualenv): """ Check pip can upgrade setuptools from source. """ @@ -104,7 +104,7 @@ def test_pip_upgrade_from_source(pip_version, virtualenv): virtualenv.run(' && '.join(( 'python setup.py -q sdist -d {dist}', 'python setup.py -q bdist_wheel -d {dist}', - )).format(dist=dist_dir), cd=SOURCE_DIR) + )).format(dist=dist_dir), cd=tmp_src) sdist = glob.glob(os.path.join(dist_dir, '*.zip'))[0] wheel = glob.glob(os.path.join(dist_dir, '*.whl'))[0] # Then update from wheel.