summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
Diffstat (limited to 'tests')
-rw-r--r--tests/conftest.py111
-rw-r--r--tests/constraints.txt9
-rw-r--r--tests/packages/inline/build.py63
-rw-r--r--tests/packages/inline/pyproject.toml4
-rw-r--r--tests/packages/legacy/legacy/__init__.py6
-rw-r--r--tests/packages/legacy/setup.py17
-rw-r--r--tests/packages/test-bad-backend/pyproject.toml3
-rw-r--r--tests/packages/test-bad-syntax/pyproject.toml2
-rw-r--r--tests/packages/test-bad-wheel/backend_bad_wheel.py7
-rw-r--r--tests/packages/test-bad-wheel/pyproject.toml4
-rw-r--r--tests/packages/test-bad-wheel/setup.cfg3
-rw-r--r--tests/packages/test-cant-build-via-sdist/backend_bad_sdist.py23
-rw-r--r--tests/packages/test-cant-build-via-sdist/pyproject.toml4
-rw-r--r--tests/packages/test-cant-build-via-sdist/some-file-that-is-needed-for-build.txt0
-rw-r--r--tests/packages/test-flit/pyproject.toml13
-rw-r--r--tests/packages/test-flit/test_flit/__init__.py6
-rw-r--r--tests/packages/test-invalid-requirements/pyproject.toml3
-rw-r--r--tests/packages/test-invalid-requirements/setup.cfg3
-rw-r--r--tests/packages/test-metadata/backend.py43
-rw-r--r--tests/packages/test-metadata/pyproject.toml14
-rw-r--r--tests/packages/test-no-backend/pyproject.toml2
-rw-r--r--tests/packages/test-no-permission/pyproject.toml0
-rw-r--r--tests/packages/test-no-prepare/backend_no_prepare.py3
-rw-r--r--tests/packages/test-no-prepare/pyproject.toml4
-rw-r--r--tests/packages/test-no-prepare/setup.cfg3
-rw-r--r--tests/packages/test-no-project/empty.txt0
-rw-r--r--tests/packages/test-no-requires/pyproject.toml2
-rw-r--r--tests/packages/test-optional-hooks/hookless_backend.py0
-rw-r--r--tests/packages/test-optional-hooks/pyproject.toml4
-rw-r--r--tests/packages/test-setuptools/pyproject.toml3
-rw-r--r--tests/packages/test-setuptools/setup.cfg6
-rw-r--r--tests/packages/test-typo/pyproject.toml3
-rw-r--r--tests/test_env.py177
-rw-r--r--tests/test_integration.py136
-rw-r--r--tests/test_main.py437
-rw-r--r--tests/test_module.py16
-rw-r--r--tests/test_projectbuilder.py672
-rw-r--r--tests/test_self_packaging.py103
-rw-r--r--tests/test_util.py44
39 files changed, 1953 insertions, 0 deletions
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..d65b64e
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,111 @@
+# SPDX-License-Identifier: MIT
+
+import os
+import os.path
+import shutil
+import stat
+import sys
+import sysconfig
+import tempfile
+
+import pytest
+
+import build.env
+
+
+def pytest_addoption(parser):
+ os.environ['PYTHONWARNINGS'] = 'ignore:DEPRECATION::pip._internal.cli.base_command' # for when not run within tox
+ os.environ['PIP_DISABLE_PIP_VERSION_CHECK'] = '1' # do not pollute stderr with upgrade advisory
+ parser.addoption('--run-integration', action='store_true', help='run the integration tests')
+ parser.addoption('--only-integration', action='store_true', help='only run the integration tests')
+
+
+PYPY3_WIN_VENV_BAD = (
+ sys.implementation.name == 'pypy' and sys.implementation.version < (7, 3, 9) and sys.platform.startswith('win')
+)
+PYPY3_WIN_M = 'https://foss.heptapod.net/pypy/pypy/-/issues/3323 and https://foss.heptapod.net/pypy/pypy/-/issues/3321'
+
+
+def pytest_collection_modifyitems(config, items):
+ skip_int = pytest.mark.skip(reason='integration tests not run (no --run-integration flag)')
+ skip_other = pytest.mark.skip(reason='only integration tests are run (got --only-integration flag)')
+
+ if config.getoption('--run-integration') and config.getoption('--only-integration'): # pragma: no cover
+ raise pytest.UsageError("--run-integration and --only-integration can't be used together, choose one")
+
+ if len(items) == 1: # do not require flags if called directly
+ return
+ for item in items:
+ is_integration_file = is_integration(item)
+ if PYPY3_WIN_VENV_BAD and item.get_closest_marker('pypy3323bug') and os.environ.get('PYPY3323BUG', None):
+ item.add_marker(pytest.mark.xfail(reason=PYPY3_WIN_M, strict=False))
+ if PYPY3_WIN_VENV_BAD and item.get_closest_marker('isolated'):
+ if not (is_integration_file and item.originalname == 'test_build') or (
+ hasattr(item, 'callspec') and '--no-isolation' not in item.callspec.params.get('args', [])
+ ):
+ item.add_marker(pytest.mark.xfail(reason=PYPY3_WIN_M, strict=True))
+ if is_integration_file: # pragma: no cover
+ if not config.getoption('--run-integration') and not config.getoption('--only-integration'):
+ item.add_marker(skip_int)
+ elif config.getoption('--only-integration'): # pragma: no cover
+ item.add_marker(skip_other)
+ # run integration tests after unit tests
+ items.sort(key=lambda i: 1 if is_integration(i) else 0)
+
+
+def is_integration(item):
+ return os.path.basename(item.location[0]) == 'test_integration.py'
+
+
+@pytest.fixture(scope='session', autouse=True)
+def ensure_syconfig_vars_created():
+ # the config vars are globally cached and may use get_path, make sure they are created
+ sysconfig.get_config_vars()
+
+
+@pytest.fixture
+def packages_path():
+ return os.path.realpath(os.path.join(__file__, '..', 'packages'))
+
+
+def generate_package_path_fixture(package_name):
+ @pytest.fixture
+ def fixture(packages_path):
+ return os.path.join(packages_path, package_name)
+
+ return fixture
+
+
+# Generate path fixtures dynamically.
+package_names = os.listdir(os.path.join(os.path.dirname(__file__), 'packages'))
+for package_name in package_names:
+ normalized_name = package_name.replace('-', '_')
+ fixture_name = f'package_{normalized_name}'
+ globals()[fixture_name] = generate_package_path_fixture(package_name)
+
+
+@pytest.fixture
+def test_no_permission(packages_path):
+ path = os.path.join(packages_path, 'test-no-permission')
+ file = os.path.join(path, 'pyproject.toml')
+ orig_stat = os.stat(file).st_mode
+
+ os.chmod(file, ~stat.S_IRWXU)
+
+ yield os.path.join(packages_path, 'test-no-permission')
+
+ os.chmod(file, orig_stat)
+
+
+@pytest.fixture
+def tmp_dir():
+ path = tempfile.mkdtemp(prefix='python-build-test-')
+
+ yield path
+
+ shutil.rmtree(path)
+
+
+@pytest.fixture(autouse=True)
+def force_venv(mocker):
+ mocker.patch.object(build.env, '_should_use_virtualenv', lambda: False)
diff --git a/tests/constraints.txt b/tests/constraints.txt
new file mode 100644
index 0000000..b073bd9
--- /dev/null
+++ b/tests/constraints.txt
@@ -0,0 +1,9 @@
+importlib-metadata==0.22
+packaging==19.0
+pep517==0.9.1
+setuptools==42.0.0; python_version < "3.10"
+setuptools==56.0.0; python_version >= "3.10"
+toml==0.10.0
+tomli==1.0.0
+virtualenv==20.0.35
+wheel==0.36.0
diff --git a/tests/packages/inline/build.py b/tests/packages/inline/build.py
new file mode 100644
index 0000000..a8e0dcb
--- /dev/null
+++ b/tests/packages/inline/build.py
@@ -0,0 +1,63 @@
+import os
+import sys
+
+from textwrap import dedent
+from zipfile import ZipFile
+
+
+name = 'demo_pkg_inline'
+pkg_name = name.replace('_', '-')
+
+version = '1.0.0'
+dist_info = f'{name}-{version}.dist-info'
+
+metadata = f'{dist_info}/METADATA'
+wheel = f'{dist_info}/WHEEL'
+entry_points = f'{dist_info}/entry_points.txt'
+record = f'{dist_info}/RECORD'
+init = f'{name}/__init__.py'
+content = {
+ init: f"def do():\n print('greetings from {name}')",
+ metadata: f"""
+ Metadata-Version: 2.1
+ Name: {pkg_name}
+ Version: {version}
+ Summary: Summary of package
+ Home-page: Does not exists
+ Author: someone
+ Author-email: a@o.com
+ License: MIT
+ Platform: ANY
+
+ Desc
+ """,
+ wheel: f"""
+ Wheel-Version: 1.0
+ Generator: {name}-{version}
+ Root-Is-Purelib: true
+ Tag: py3-none-any
+ """,
+ f'{dist_info}/top_level.txt': name,
+ entry_points: '\n[console_scripts]\ndemo-pkg-inline = demo_pkg_inline:do',
+ record: f"""
+ {name}/__init__.py,,
+ {dist_info}/METADATA,,
+ {dist_info}/WHEEL,,
+ {dist_info}/top_level.txt,,
+ {dist_info}/RECORD,,
+ """,
+}
+
+
+def build_wheel(wheel_directory, metadata_directory=None, config_settings=None):
+ base_name = f'{name}-{version}-py{sys.version_info.major}-none-any.whl'
+ path = os.path.join(wheel_directory, base_name)
+ with ZipFile(str(path), 'w') as zip_file_handler:
+ for arc_name, data in content.items():
+ zip_file_handler.writestr(arc_name, dedent(data).strip())
+ print(f'created wheel {path}')
+ return base_name
+
+
+def get_requires_for_build_wheel(config_settings):
+ return []
diff --git a/tests/packages/inline/pyproject.toml b/tests/packages/inline/pyproject.toml
new file mode 100644
index 0000000..dc9ecbb
--- /dev/null
+++ b/tests/packages/inline/pyproject.toml
@@ -0,0 +1,4 @@
+[build-system]
+requires = []
+build-backend = "build"
+backend-path = ["."]
diff --git a/tests/packages/legacy/legacy/__init__.py b/tests/packages/legacy/legacy/__init__.py
new file mode 100644
index 0000000..d5a030b
--- /dev/null
+++ b/tests/packages/legacy/legacy/__init__.py
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: MIT
+
+"""
+legacy - Example legacy package
+"""
+__version__ = '1.0.0'
diff --git a/tests/packages/legacy/setup.py b/tests/packages/legacy/setup.py
new file mode 100644
index 0000000..9feee16
--- /dev/null
+++ b/tests/packages/legacy/setup.py
@@ -0,0 +1,17 @@
+#!/usr/bin/env python
+
+from setuptools import setup
+
+
+setup(
+ name='legacy',
+ version='1.0.0',
+ author='Filipe Laíns',
+ author_email='lains@archlinux.org',
+ classifiers=[
+ 'License :: OSI Approved :: MIT License',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ ],
+ packages=['legacy'],
+)
diff --git a/tests/packages/test-bad-backend/pyproject.toml b/tests/packages/test-bad-backend/pyproject.toml
new file mode 100644
index 0000000..c199a33
--- /dev/null
+++ b/tests/packages/test-bad-backend/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = []
+build-backend = "nonsense_package"
diff --git a/tests/packages/test-bad-syntax/pyproject.toml b/tests/packages/test-bad-syntax/pyproject.toml
new file mode 100644
index 0000000..cb84496
--- /dev/null
+++ b/tests/packages/test-bad-syntax/pyproject.toml
@@ -0,0 +1,2 @@
+[build-system]
+requires = ['bad' 'syntax']
diff --git a/tests/packages/test-bad-wheel/backend_bad_wheel.py b/tests/packages/test-bad-wheel/backend_bad_wheel.py
new file mode 100644
index 0000000..039cae0
--- /dev/null
+++ b/tests/packages/test-bad-wheel/backend_bad_wheel.py
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: MIT
+
+from setuptools.build_meta import build_sdist # noqa: F401
+
+
+def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
+ return 'not a wheel'
diff --git a/tests/packages/test-bad-wheel/pyproject.toml b/tests/packages/test-bad-wheel/pyproject.toml
new file mode 100644
index 0000000..77de5df
--- /dev/null
+++ b/tests/packages/test-bad-wheel/pyproject.toml
@@ -0,0 +1,4 @@
+[build-system]
+build-backend = 'backend_bad_wheel'
+backend-path = ['.']
+requires = ['setuptools >= 42.0.0']
diff --git a/tests/packages/test-bad-wheel/setup.cfg b/tests/packages/test-bad-wheel/setup.cfg
new file mode 100644
index 0000000..1a4be2f
--- /dev/null
+++ b/tests/packages/test-bad-wheel/setup.cfg
@@ -0,0 +1,3 @@
+[metadata]
+name = test_bad_wheel
+version = 0.0.1
diff --git a/tests/packages/test-cant-build-via-sdist/backend_bad_sdist.py b/tests/packages/test-cant-build-via-sdist/backend_bad_sdist.py
new file mode 100644
index 0000000..2afdb3d
--- /dev/null
+++ b/tests/packages/test-cant-build-via-sdist/backend_bad_sdist.py
@@ -0,0 +1,23 @@
+# SPDX-License-Identifier: MIT
+
+import os.path
+import tarfile
+import zipfile
+
+
+def build_sdist(sdist_directory, config_settings=None):
+ name = 'test_cant_build_via_sdist-1.0.0'
+ file = f'{name}.tar.gz'
+ with tarfile.open(os.path.join(sdist_directory, file), 'w') as t:
+ t.add('pyproject.toml', f'{name}/pyproject.toml')
+ t.add('backend_bad_sdist.py', f'{name}/backend_bad_sdist.py')
+ return file
+
+
+def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
+ if not os.path.isfile('some-file-that-is-needed-for-build.txt'):
+ raise FileNotFoundError('some-file-that-is-needed-for-build.txt is missing!')
+ # pragma: no cover
+ file = 'test_cant_build_via_sdist-1.0.0-py2.py3-none-any.whl'
+ zipfile.ZipFile(os.path.join(wheel_directory, file), 'w').close()
+ return file
diff --git a/tests/packages/test-cant-build-via-sdist/pyproject.toml b/tests/packages/test-cant-build-via-sdist/pyproject.toml
new file mode 100644
index 0000000..e74afad
--- /dev/null
+++ b/tests/packages/test-cant-build-via-sdist/pyproject.toml
@@ -0,0 +1,4 @@
+[build-system]
+build-backend = 'backend_bad_sdist'
+backend-path = ['.']
+requires = []
diff --git a/tests/packages/test-cant-build-via-sdist/some-file-that-is-needed-for-build.txt b/tests/packages/test-cant-build-via-sdist/some-file-that-is-needed-for-build.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/packages/test-cant-build-via-sdist/some-file-that-is-needed-for-build.txt
diff --git a/tests/packages/test-flit/pyproject.toml b/tests/packages/test-flit/pyproject.toml
new file mode 100644
index 0000000..9758b57
--- /dev/null
+++ b/tests/packages/test-flit/pyproject.toml
@@ -0,0 +1,13 @@
+[build-system]
+requires = ['flit_core >=2,<3']
+build-backend = 'flit_core.buildapi'
+
+[tool.flit.metadata]
+module = 'test_flit'
+author = 'Filipe Laíns'
+author-email = 'lains@archlinux.org'
+classifiers = [
+ 'License :: OSI Approved :: MIT License',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+]
diff --git a/tests/packages/test-flit/test_flit/__init__.py b/tests/packages/test-flit/test_flit/__init__.py
new file mode 100644
index 0000000..264c2e3
--- /dev/null
+++ b/tests/packages/test-flit/test_flit/__init__.py
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: MIT
+
+"""
+test_flit - Example flit package
+"""
+__version__ = '1.0.0'
diff --git a/tests/packages/test-invalid-requirements/pyproject.toml b/tests/packages/test-invalid-requirements/pyproject.toml
new file mode 100644
index 0000000..11974a0
--- /dev/null
+++ b/tests/packages/test-invalid-requirements/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ['setuptools >= 42.0.0', 'wheel >= 0.36.0', 'this is invalid']
+build-backend = 'setuptools.build_meta'
diff --git a/tests/packages/test-invalid-requirements/setup.cfg b/tests/packages/test-invalid-requirements/setup.cfg
new file mode 100644
index 0000000..aa22d23
--- /dev/null
+++ b/tests/packages/test-invalid-requirements/setup.cfg
@@ -0,0 +1,3 @@
+[metadata]
+name = test_invalid_requirements
+version = 1.0.0
diff --git a/tests/packages/test-metadata/backend.py b/tests/packages/test-metadata/backend.py
new file mode 100644
index 0000000..727dab9
--- /dev/null
+++ b/tests/packages/test-metadata/backend.py
@@ -0,0 +1,43 @@
+# SPDX-License-Identifier: MIT
+
+import pathlib
+import textwrap
+
+
+def get_requires_for_build_wheel(config_settings=None):
+ return ['tomli']
+
+
+def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
+ import tomli
+
+ with open('pyproject.toml', 'rb') as f:
+ metadata = tomli.load(f)
+
+ distinfo = pathlib.Path(
+ metadata_directory,
+ '{}-{}.dist-info'.format(
+ metadata['project']['name'].replace('-', '-'),
+ metadata['project']['version'],
+ ),
+ )
+ distinfo.mkdir(parents=True, exist_ok=True)
+ distinfo.joinpath('METADATA').write_text(
+ textwrap.dedent(
+ f'''
+ Metadata-Version: 2.2
+ Name: {metadata['project']['name']}
+ Version: {metadata['project']['version']}
+ Summary: {metadata['project']['description']}
+ '''
+ ).strip()
+ )
+ return distinfo.name
+
+
+def build_wheel(wheel_directory, config_settings=None, metadata_directory=None):
+ raise NotImplementedError
+
+
+def build_sdist(sdist_directory, config_settings=None):
+ raise NotImplementedError
diff --git a/tests/packages/test-metadata/pyproject.toml b/tests/packages/test-metadata/pyproject.toml
new file mode 100644
index 0000000..3900263
--- /dev/null
+++ b/tests/packages/test-metadata/pyproject.toml
@@ -0,0 +1,14 @@
+[build-system]
+requires = []
+build-backend = 'backend'
+backend-path = ['.']
+
+[project]
+name = 'test-metadata'
+version = '1.0.0'
+description = 'hello!'
+
+[tool.black]
+line-length = 127
+skip-string-normalization = true
+target-version = ['py39', 'py38', 'py37', 'py36']
diff --git a/tests/packages/test-no-backend/pyproject.toml b/tests/packages/test-no-backend/pyproject.toml
new file mode 100644
index 0000000..024e9e6
--- /dev/null
+++ b/tests/packages/test-no-backend/pyproject.toml
@@ -0,0 +1,2 @@
+[build-system]
+requires = []
diff --git a/tests/packages/test-no-permission/pyproject.toml b/tests/packages/test-no-permission/pyproject.toml
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/packages/test-no-permission/pyproject.toml
diff --git a/tests/packages/test-no-prepare/backend_no_prepare.py b/tests/packages/test-no-prepare/backend_no_prepare.py
new file mode 100644
index 0000000..206de1a
--- /dev/null
+++ b/tests/packages/test-no-prepare/backend_no_prepare.py
@@ -0,0 +1,3 @@
+# SPDX-License-Identifier: MIT
+
+from setuptools.build_meta import build_sdist, build_wheel # noqa: F401
diff --git a/tests/packages/test-no-prepare/pyproject.toml b/tests/packages/test-no-prepare/pyproject.toml
new file mode 100644
index 0000000..c6ca5f8
--- /dev/null
+++ b/tests/packages/test-no-prepare/pyproject.toml
@@ -0,0 +1,4 @@
+[build-system]
+build-backend = 'backend_no_prepare'
+backend-path = ['.']
+requires = ['setuptools >= 42.0.0', 'wheel >= 0.36.0']
diff --git a/tests/packages/test-no-prepare/setup.cfg b/tests/packages/test-no-prepare/setup.cfg
new file mode 100644
index 0000000..c9e0a96
--- /dev/null
+++ b/tests/packages/test-no-prepare/setup.cfg
@@ -0,0 +1,3 @@
+[metadata]
+name = test_no_prepare
+version = 1.0.0
diff --git a/tests/packages/test-no-project/empty.txt b/tests/packages/test-no-project/empty.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/packages/test-no-project/empty.txt
diff --git a/tests/packages/test-no-requires/pyproject.toml b/tests/packages/test-no-requires/pyproject.toml
new file mode 100644
index 0000000..4f25948
--- /dev/null
+++ b/tests/packages/test-no-requires/pyproject.toml
@@ -0,0 +1,2 @@
+[build-system]
+build-backend = 'something'
diff --git a/tests/packages/test-optional-hooks/hookless_backend.py b/tests/packages/test-optional-hooks/hookless_backend.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/packages/test-optional-hooks/hookless_backend.py
diff --git a/tests/packages/test-optional-hooks/pyproject.toml b/tests/packages/test-optional-hooks/pyproject.toml
new file mode 100644
index 0000000..2796891
--- /dev/null
+++ b/tests/packages/test-optional-hooks/pyproject.toml
@@ -0,0 +1,4 @@
+[build-system]
+requires = []
+build-backend = 'hookless_backend'
+backend-path = ['.']
diff --git a/tests/packages/test-setuptools/pyproject.toml b/tests/packages/test-setuptools/pyproject.toml
new file mode 100644
index 0000000..b00a27a
--- /dev/null
+++ b/tests/packages/test-setuptools/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ['setuptools >= 42.0.0', 'wheel >= 0.36.0']
+build-backend = 'setuptools.build_meta'
diff --git a/tests/packages/test-setuptools/setup.cfg b/tests/packages/test-setuptools/setup.cfg
new file mode 100644
index 0000000..bf198b6
--- /dev/null
+++ b/tests/packages/test-setuptools/setup.cfg
@@ -0,0 +1,6 @@
+[metadata]
+name = test_setuptools
+version = 1.0.0
+
+[bdist_wheel]
+universal = 1
diff --git a/tests/packages/test-typo/pyproject.toml b/tests/packages/test-typo/pyproject.toml
new file mode 100644
index 0000000..02d1af2
--- /dev/null
+++ b/tests/packages/test-typo/pyproject.toml
@@ -0,0 +1,3 @@
+[build_sytem]
+requires = ['setuptools >= 40.8.0', 'wheel']
+build-backend = 'setuptools.build_meta'
diff --git a/tests/test_env.py b/tests/test_env.py
new file mode 100644
index 0000000..f6f381a
--- /dev/null
+++ b/tests/test_env.py
@@ -0,0 +1,177 @@
+# SPDX-License-Identifier: MIT
+import collections
+import inspect
+import logging
+import platform
+import subprocess
+import sys
+import sysconfig
+
+import pytest
+
+from packaging.version import Version
+
+import build.env
+
+
+IS_PYPY3 = platform.python_implementation() == 'PyPy'
+
+
+@pytest.mark.isolated
+def test_isolation():
+ subprocess.check_call([sys.executable, '-c', 'import build.env'])
+ with build.env.IsolatedEnvBuilder() as env:
+ with pytest.raises(subprocess.CalledProcessError):
+ debug = 'import sys; import os; print(os.linesep.join(sys.path));'
+ subprocess.check_call([env.executable, '-c', f'{debug} import build.env'])
+
+
+@pytest.mark.isolated
+def test_isolated_environment_install(mocker):
+ with build.env.IsolatedEnvBuilder() as env:
+ mocker.patch('build.env._subprocess')
+
+ env.install([])
+ build.env._subprocess.assert_not_called()
+
+ env.install(['some', 'requirements'])
+ build.env._subprocess.assert_called()
+ args = build.env._subprocess.call_args[0][0][:-1]
+ assert args == [
+ env.executable,
+ '-Im',
+ 'pip',
+ 'install',
+ '--use-pep517',
+ '--no-warn-script-location',
+ '-r',
+ ]
+
+
+@pytest.mark.skipif(IS_PYPY3, reason='PyPy3 uses get path to create and provision venv')
+@pytest.mark.skipif(sys.platform != 'darwin', reason='workaround for Apple Python')
+def test_can_get_venv_paths_with_conflicting_default_scheme(mocker):
+ get_scheme_names = mocker.patch('sysconfig.get_scheme_names', return_value=('osx_framework_library',))
+ with build.env.IsolatedEnvBuilder():
+ pass
+ assert get_scheme_names.call_count == 1
+
+
+@pytest.mark.skipif('posix_local' not in sysconfig.get_scheme_names(), reason='workaround for Debian/Ubuntu Python')
+def test_can_get_venv_paths_with_posix_local_default_scheme(mocker):
+ get_paths = mocker.spy(sysconfig, 'get_paths')
+ # We should never call this, but we patch it to ensure failure if we do
+ get_default_scheme = mocker.patch('sysconfig.get_default_scheme', return_value='posix_local')
+ with build.env.IsolatedEnvBuilder():
+ pass
+ get_paths.assert_called_once_with(scheme='posix_prefix', vars=mocker.ANY)
+ assert get_default_scheme.call_count == 0
+
+
+def test_executable_missing_post_creation(mocker):
+ venv_create = mocker.patch('venv.EnvBuilder.create')
+ with pytest.raises(RuntimeError, match='Virtual environment creation failed, executable .* missing'):
+ with build.env.IsolatedEnvBuilder():
+ pass
+ assert venv_create.call_count == 1
+
+
+def test_isolated_env_abstract():
+ with pytest.raises(TypeError):
+ build.env.IsolatedEnv()
+
+
+def test_isolated_env_has_executable_still_abstract():
+ class Env(build.env.IsolatedEnv):
+ @property
+ def executable(self):
+ raise NotImplementedError
+
+ with pytest.raises(TypeError):
+ Env()
+
+
+def test_isolated_env_has_install_still_abstract():
+ class Env(build.env.IsolatedEnv):
+ def install(self, requirements):
+ raise NotImplementedError
+
+ with pytest.raises(TypeError):
+ Env()
+
+
+@pytest.mark.pypy3323bug
+def test_isolated_env_log(mocker, caplog, package_test_flit):
+ mocker.patch('build.env._subprocess')
+ caplog.set_level(logging.DEBUG)
+
+ builder = build.env.IsolatedEnvBuilder()
+ frameinfo = inspect.getframeinfo(inspect.currentframe())
+ builder.log('something') # line number 106
+ with builder as env:
+ env.install(['something'])
+
+ assert [(record.levelname, record.message) for record in caplog.records] == [
+ ('INFO', 'something'),
+ ('INFO', 'Creating venv isolated environment...'),
+ ('INFO', 'Installing packages in isolated environment... (something)'),
+ ]
+ if sys.version_info >= (3, 8): # stacklevel
+ assert [(record.lineno) for record in caplog.records] == [
+ frameinfo.lineno + 1,
+ frameinfo.lineno - 6,
+ frameinfo.lineno + 85,
+ ]
+
+
+@pytest.mark.isolated
+def test_default_pip_is_never_too_old():
+ with build.env.IsolatedEnvBuilder() as env:
+ version = subprocess.check_output(
+ [env.executable, '-c', 'import pip; print(pip.__version__)'], universal_newlines=True
+ ).strip()
+ assert Version(version) >= Version('19.1')
+
+
+@pytest.mark.isolated
+@pytest.mark.parametrize('pip_version', ['20.2.0', '20.3.0', '21.0.0', '21.0.1'])
+@pytest.mark.parametrize('arch', ['x86_64', 'arm64'])
+def test_pip_needs_upgrade_mac_os_11(mocker, pip_version, arch):
+ SimpleNamespace = collections.namedtuple('SimpleNamespace', 'version')
+
+ _subprocess = mocker.patch('build.env._subprocess')
+ mocker.patch('platform.system', return_value='Darwin')
+ mocker.patch('platform.machine', return_value=arch)
+ mocker.patch('platform.mac_ver', return_value=('11.0', ('', '', ''), ''))
+ metadata_name = 'importlib_metadata' if sys.version_info < (3, 8) else 'importlib.metadata'
+ mocker.patch(metadata_name + '.distributions', return_value=(SimpleNamespace(version=pip_version),))
+
+ min_version = Version('20.3' if arch == 'x86_64' else '21.0.1')
+ with build.env.IsolatedEnvBuilder():
+ if Version(pip_version) < min_version:
+ print(_subprocess.call_args_list)
+ upgrade_call, uninstall_call = _subprocess.call_args_list
+ answer = 'pip>=20.3.0' if arch == 'x86_64' else 'pip>=21.0.1'
+ assert upgrade_call[0][0][1:] == ['-m', 'pip', 'install', answer]
+ assert uninstall_call[0][0][1:] == ['-m', 'pip', 'uninstall', 'setuptools', '-y']
+ else:
+ (uninstall_call,) = _subprocess.call_args_list
+ assert uninstall_call[0][0][1:] == ['-m', 'pip', 'uninstall', 'setuptools', '-y']
+
+
+@pytest.mark.isolated
+@pytest.mark.skipif(IS_PYPY3 and sys.platform.startswith('win'), reason='Isolated tests not supported on PyPy3 + Windows')
+@pytest.mark.parametrize('has_symlink', [True, False] if sys.platform.startswith('win') else [True])
+def test_venv_symlink(mocker, has_symlink):
+ if has_symlink:
+ mocker.patch('os.symlink')
+ mocker.patch('os.unlink')
+ else:
+ mocker.patch('os.symlink', side_effect=OSError())
+
+ # Cache must be cleared to rerun
+ build.env._fs_supports_symlink.cache_clear()
+ supports_symlink = build.env._fs_supports_symlink()
+ build.env._fs_supports_symlink.cache_clear()
+
+ assert supports_symlink is has_symlink
diff --git a/tests/test_integration.py b/tests/test_integration.py
new file mode 100644
index 0000000..bc2f4ff
--- /dev/null
+++ b/tests/test_integration.py
@@ -0,0 +1,136 @@
+# SPDX-License-Identifier: MIT
+
+import os
+import os.path
+import platform
+import re
+import shutil
+import subprocess
+import sys
+import tarfile
+import urllib.request
+
+import filelock
+import pytest
+
+import build.__main__
+
+
+IS_WINDOWS = sys.platform.startswith('win')
+IS_PYPY3 = platform.python_implementation() == 'PyPy'
+
+
+INTEGRATION_SOURCES = {
+ 'dateutil': ('dateutil/dateutil', '2.8.1'),
+ 'pip': ('pypa/pip', '20.2.1'),
+ 'Solaar': ('pwr-Solaar/Solaar', '1.0.3'),
+ 'flit': ('takluyver/flit', '2.3.0'),
+}
+
+_SDIST = re.compile('.*.tar.gz')
+_WHEEL = re.compile('.*.whl')
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+def get_project(name, tmp_path):
+ dest = tmp_path / name
+ if name == 'build':
+ # our own project is available in-source, just ignore development files
+
+ def _ignore_folder(base, filenames):
+ ignore = [n for n in filenames if n in excl or any(n.endswith(i) for i in ('_cache', '.egg-info', '.pyc'))]
+ if os.path.basename == ROOT and 'build' in filenames: # ignore build only at root (our module is build too)
+ ignore.append('build')
+ return ignore
+
+ excl = '.tox', 'dist', '.git', '__pycache__', '.integration-sources', '.github', 'tests', 'docs'
+ shutil.copytree(ROOT, str(dest), ignore=_ignore_folder)
+ return dest
+
+ # for other projects download from github and cache it
+ tar_store = os.path.join(ROOT, '.integration-sources')
+ try:
+ os.makedirs(tar_store)
+ except OSError: # python 2 has no exist_ok, and checking with exists is not parallel safe
+ pass # just ignore, if the creation failed we will have another failure soon that will notify the user
+
+ github_org_repo, version = INTEGRATION_SOURCES[name]
+ tar_filename = f'{name}-{version}.tar.gz'
+ tarball = os.path.join(tar_store, tar_filename)
+ with filelock.FileLock(os.path.join(tar_store, f'{tar_filename}.lock')):
+ if not os.path.exists(tarball):
+ url = f'https://github.com/{github_org_repo}/archive/{version}.tar.gz'
+ with urllib.request.urlopen(url) as request, open(tarball, 'wb') as file_handler:
+ shutil.copyfileobj(request, file_handler)
+ with tarfile.open(tarball, 'r:gz') as tar_handler:
+ tar_handler.extractall(str(dest))
+ return dest / f'{name}-{version}'
+
+
+@pytest.mark.parametrize(
+ 'call',
+ [
+ None, # via code
+ [sys.executable, '-m', 'build'], # module
+ ['pyproject-build'], # entrypoint
+ ],
+ ids=['code', 'module', 'entrypoint'],
+)
+@pytest.mark.parametrize(
+ 'args',
+ [[], ['-x', '--no-isolation']],
+ ids=['isolated', 'no_isolation'],
+)
+@pytest.mark.parametrize(
+ 'project',
+ [
+ 'build',
+ 'pip',
+ 'dateutil',
+ 'Solaar',
+ 'flit',
+ ],
+)
+@pytest.mark.isolated
+def test_build(monkeypatch, project, args, call, tmp_path):
+ if project == 'flit' and '--no-isolation' in args:
+ pytest.xfail("can't build flit without isolation due to missing dependencies")
+ if project == 'Solaar' and IS_WINDOWS and IS_PYPY3:
+ pytest.xfail('Solaar fails building wheels via sdists on Windows on PyPy 3')
+
+ monkeypatch.chdir(tmp_path)
+ monkeypatch.setenv('SETUPTOOLS_SCM_PRETEND_VERSION', '0+dummy') # for the projects that use setuptools_scm
+
+ if call and call[0] == 'pyproject-build':
+ exe_name = f"pyproject-build{'.exe' if sys.platform.startswith('win') else ''}"
+ exe = os.path.join(os.path.dirname(sys.executable), exe_name)
+ if os.path.exists(exe):
+ call[0] = exe
+ else:
+ pytest.skip('Running via PYTHONPATH, so the pyproject-build entrypoint is not available')
+ path = get_project(project, tmp_path)
+ pkgs = tmp_path / 'pkgs'
+ args = [str(path), '-o', str(pkgs)] + args
+
+ if call is None:
+ build.__main__.main(args)
+ else:
+ subprocess.check_call(call + args)
+
+ pkg_names = os.listdir(str(pkgs))
+ assert list(filter(_SDIST.match, pkg_names))
+ assert list(filter(_WHEEL.match, pkg_names))
+
+
+def test_isolation(tmp_dir, package_test_flit, mocker):
+ try:
+ import flit_core # noqa: F401
+ except ModuleNotFoundError:
+ pass
+ else:
+ pytest.xfail('flit_core is available -- we want it missing!') # pragma: no cover
+
+ mocker.patch('build.__main__._error')
+
+ build.__main__.main([package_test_flit, '-o', tmp_dir, '--no-isolation'])
+ build.__main__._error.assert_called_with("Backend 'flit_core.buildapi' is not available.")
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644
index 0000000..e1fbe0c
--- /dev/null
+++ b/tests/test_main.py
@@ -0,0 +1,437 @@
+# SPDX-License-Identifier: MIT
+
+import contextlib
+import importlib
+import io
+import os
+import re
+import subprocess
+import sys
+import venv
+
+import pytest
+
+import build
+import build.__main__
+
+
+build_open_owner = 'builtins'
+
+cwd = os.getcwd()
+out = os.path.join(cwd, 'dist')
+
+
+@pytest.mark.parametrize(
+ ('cli_args', 'build_args', 'hook'),
+ [
+ (
+ [],
+ [cwd, out, ['wheel'], {}, True, False],
+ 'build_package_via_sdist',
+ ),
+ (
+ ['-n'],
+ [cwd, out, ['wheel'], {}, False, False],
+ 'build_package_via_sdist',
+ ),
+ (
+ ['-s'],
+ [cwd, out, ['sdist'], {}, True, False],
+ 'build_package',
+ ),
+ (
+ ['-w'],
+ [cwd, out, ['wheel'], {}, True, False],
+ 'build_package',
+ ),
+ (
+ ['-s', '-w'],
+ [cwd, out, ['sdist', 'wheel'], {}, True, False],
+ 'build_package',
+ ),
+ (
+ ['source'],
+ ['source', os.path.join('source', 'dist'), ['wheel'], {}, True, False],
+ 'build_package_via_sdist',
+ ),
+ (
+ ['-o', 'out'],
+ [cwd, 'out', ['wheel'], {}, True, False],
+ 'build_package_via_sdist',
+ ),
+ (
+ ['source', '-o', 'out'],
+ ['source', 'out', ['wheel'], {}, True, False],
+ 'build_package_via_sdist',
+ ),
+ (
+ ['-x'],
+ [cwd, out, ['wheel'], {}, True, True],
+ 'build_package_via_sdist',
+ ),
+ (
+ ['-C--flag1', '-C--flag2'],
+ [cwd, out, ['wheel'], {'--flag1': '', '--flag2': ''}, True, False],
+ 'build_package_via_sdist',
+ ),
+ (
+ ['-C--flag=value'],
+ [cwd, out, ['wheel'], {'--flag': 'value'}, True, False],
+ 'build_package_via_sdist',
+ ),
+ (
+ ['-C--flag1=value', '-C--flag2=other_value', '-C--flag2=extra_value'],
+ [cwd, out, ['wheel'], {'--flag1': 'value', '--flag2': ['other_value', 'extra_value']}, True, False],
+ 'build_package_via_sdist',
+ ),
+ ],
+)
+def test_parse_args(mocker, cli_args, build_args, hook):
+ mocker.patch('build.__main__.build_package', return_value=['something'])
+ mocker.patch('build.__main__.build_package_via_sdist', return_value=['something'])
+
+ build.__main__.main(cli_args)
+
+ if hook == 'build_package':
+ build.__main__.build_package.assert_called_with(*build_args)
+ elif hook == 'build_package_via_sdist':
+ build.__main__.build_package_via_sdist.assert_called_with(*build_args)
+ else:
+ raise ValueError(f'Unknown hook {hook}') # pragma: no cover
+
+
+def test_prog():
+ out = io.StringIO()
+
+ with pytest.raises(SystemExit):
+ with contextlib.redirect_stdout(out):
+ build.__main__.main(['--help'], prog='something')
+
+ assert out.getvalue().startswith('usage: something [-h]')
+
+
+def test_version(capsys):
+ with pytest.raises(SystemExit):
+ build.__main__.main(['--version'])
+ out, err = capsys.readouterr()
+ assert out.startswith(f'build {build.__version__}')
+
+
+@pytest.mark.isolated
+def test_build_isolated(mocker, package_test_flit):
+ build_cmd = mocker.patch('build.ProjectBuilder.build', return_value='something')
+ required_cmd = mocker.patch(
+ 'build.ProjectBuilder.get_requires_for_build',
+ side_effect=[
+ ['dep1', 'dep2'],
+ ],
+ )
+ mocker.patch('build.__main__._error')
+ install = mocker.patch('build.env._IsolatedEnvVenvPip.install')
+
+ build.__main__.build_package(package_test_flit, '.', ['sdist'])
+
+ install.assert_any_call({'flit_core >=2,<3'})
+
+ required_cmd.assert_called_with('sdist')
+ install.assert_any_call(['dep1', 'dep2'])
+
+ build_cmd.assert_called_with('sdist', '.', {})
+
+
+def test_build_no_isolation_check_deps_empty(mocker, package_test_flit):
+ # check_dependencies = []
+ build_cmd = mocker.patch('build.ProjectBuilder.build', return_value='something')
+ mocker.patch('build.ProjectBuilder.check_dependencies', return_value=[])
+
+ build.__main__.build_package(package_test_flit, '.', ['sdist'], isolation=False)
+
+ build_cmd.assert_called_with('sdist', '.', {})
+
+
+@pytest.mark.parametrize(
+ ['missing_deps', 'output'],
+ [
+ ([('foo',)], '\n\tfoo'),
+ ([('foo',), ('bar', 'baz', 'qux')], '\n\tfoo\n\tbar\n\tbaz -> qux'),
+ ],
+)
+def test_build_no_isolation_with_check_deps(mocker, package_test_flit, missing_deps, output):
+ error = mocker.patch('build.__main__._error')
+ build_cmd = mocker.patch('build.ProjectBuilder.build', return_value='something')
+ mocker.patch('build.ProjectBuilder.check_dependencies', return_value=missing_deps)
+
+ build.__main__.build_package(package_test_flit, '.', ['sdist'], isolation=False)
+
+ build_cmd.assert_called_with('sdist', '.', {})
+ error.assert_called_with('Missing dependencies:' + output)
+
+
+@pytest.mark.isolated
+def test_build_raises_build_exception(mocker, package_test_flit):
+ mocker.patch('build.ProjectBuilder.get_requires_for_build', side_effect=build.BuildException)
+ mocker.patch('build.env._IsolatedEnvVenvPip.install')
+
+ with pytest.raises(build.BuildException):
+ build.__main__.build_package(package_test_flit, '.', ['sdist'])
+
+
+@pytest.mark.isolated
+def test_build_raises_build_backend_exception(mocker, package_test_flit):
+ mocker.patch('build.ProjectBuilder.get_requires_for_build', side_effect=build.BuildBackendException(Exception('a')))
+ mocker.patch('build.env._IsolatedEnvVenvPip.install')
+
+ msg = f"Backend operation failed: Exception('a'{',' if sys.version_info < (3, 7) else ''})"
+ with pytest.raises(build.BuildBackendException, match=re.escape(msg)):
+ build.__main__.build_package(package_test_flit, '.', ['sdist'])
+
+
+@pytest.mark.pypy3323bug
+def test_build_package(tmp_dir, package_test_setuptools):
+ build.__main__.build_package(package_test_setuptools, tmp_dir, ['sdist', 'wheel'])
+
+ assert sorted(os.listdir(tmp_dir)) == [
+ 'test_setuptools-1.0.0-py2.py3-none-any.whl',
+ 'test_setuptools-1.0.0.tar.gz',
+ ]
+
+
+@pytest.mark.pypy3323bug
+def test_build_package_via_sdist(tmp_dir, package_test_setuptools):
+ build.__main__.build_package_via_sdist(package_test_setuptools, tmp_dir, ['wheel'])
+
+ assert sorted(os.listdir(tmp_dir)) == [
+ 'test_setuptools-1.0.0-py2.py3-none-any.whl',
+ 'test_setuptools-1.0.0.tar.gz',
+ ]
+
+
+@pytest.mark.pypy3323bug
+def test_build_package_via_sdist_cant_build(tmp_dir, package_test_cant_build_via_sdist):
+ with pytest.raises(build.BuildBackendException):
+ build.__main__.build_package_via_sdist(package_test_cant_build_via_sdist, tmp_dir, ['wheel'])
+
+
+def test_build_package_via_sdist_invalid_distribution(tmp_dir, package_test_setuptools):
+ with pytest.raises(ValueError, match='Only binary distributions are allowed but sdist was specified'):
+ build.__main__.build_package_via_sdist(package_test_setuptools, tmp_dir, ['sdist'])
+
+
+@pytest.mark.pypy3323bug
+@pytest.mark.parametrize(
+ ('args', 'output'),
+ [
+ (
+ [],
+ [
+ '* Creating venv isolated environment...',
+ '* Installing packages in isolated environment... (setuptools >= 42.0.0, wheel >= 0.36.0)',
+ '* Getting build dependencies for sdist...',
+ '* Building sdist...',
+ '* Building wheel from sdist',
+ '* Creating venv isolated environment...',
+ '* Installing packages in isolated environment... (setuptools >= 42.0.0, wheel >= 0.36.0)',
+ '* Getting build dependencies for wheel...',
+ '* Installing packages in isolated environment... (wheel)',
+ '* Building wheel...',
+ 'Successfully built test_setuptools-1.0.0.tar.gz and test_setuptools-1.0.0-py2.py3-none-any.whl',
+ ],
+ ),
+ (
+ ['--no-isolation'],
+ [
+ '* Getting build dependencies for sdist...',
+ '* Building sdist...',
+ '* Building wheel from sdist',
+ '* Getting build dependencies for wheel...',
+ '* Building wheel...',
+ 'Successfully built test_setuptools-1.0.0.tar.gz and test_setuptools-1.0.0-py2.py3-none-any.whl',
+ ],
+ ),
+ (
+ ['--wheel'],
+ [
+ '* Creating venv isolated environment...',
+ '* Installing packages in isolated environment... (setuptools >= 42.0.0, wheel >= 0.36.0)',
+ '* Getting build dependencies for wheel...',
+ '* Installing packages in isolated environment... (wheel)',
+ '* Building wheel...',
+ 'Successfully built test_setuptools-1.0.0-py2.py3-none-any.whl',
+ ],
+ ),
+ (
+ ['--wheel', '--no-isolation'],
+ [
+ '* Getting build dependencies for wheel...',
+ '* Building wheel...',
+ 'Successfully built test_setuptools-1.0.0-py2.py3-none-any.whl',
+ ],
+ ),
+ (
+ ['--sdist', '--no-isolation'],
+ [
+ '* Getting build dependencies for sdist...',
+ '* Building sdist...',
+ 'Successfully built test_setuptools-1.0.0.tar.gz',
+ ],
+ ),
+ (
+ ['--sdist', '--wheel', '--no-isolation'],
+ [
+ '* Getting build dependencies for sdist...',
+ '* Building sdist...',
+ '* Getting build dependencies for wheel...',
+ '* Building wheel...',
+ 'Successfully built test_setuptools-1.0.0.tar.gz and test_setuptools-1.0.0-py2.py3-none-any.whl',
+ ],
+ ),
+ ],
+ ids=[
+ 'via-sdist-isolation',
+ 'via-sdist-no-isolation',
+ 'wheel-direct-isolation',
+ 'wheel-direct-no-isolation',
+ 'sdist-direct-no-isolation',
+ 'sdist-and-wheel-direct-no-isolation',
+ ],
+)
+@pytest.mark.flaky(reruns=5)
+def test_output(package_test_setuptools, tmp_dir, capsys, args, output):
+ build.__main__.main([package_test_setuptools, '-o', tmp_dir] + args)
+ stdout, stderr = capsys.readouterr()
+ assert stdout.splitlines() == output
+
+
+@pytest.fixture()
+def main_reload_styles():
+ try:
+ yield
+ finally:
+ importlib.reload(build.__main__)
+
+
+@pytest.mark.pypy3323bug
+@pytest.mark.parametrize(
+ ('color', 'stdout_error', 'stdout_body'),
+ [
+ (
+ False,
+ 'ERROR ',
+ [
+ '* Creating venv isolated environment...',
+ '* Installing packages in isolated environment... (setuptools >= 42.0.0, this is invalid, wheel >= 0.36.0)',
+ '',
+ 'Traceback (most recent call last):',
+ ],
+ ),
+ (
+ True,
+ '\33[91mERROR\33[0m ',
+ [
+ '\33[1m* Creating venv isolated environment...\33[0m',
+ '\33[1m* Installing packages in isolated environment... '
+ '(setuptools >= 42.0.0, this is invalid, wheel >= 0.36.0)\33[0m',
+ '',
+ '\33[2mTraceback (most recent call last):',
+ ],
+ ),
+ ],
+ ids=['no-color', 'color'],
+)
+def test_output_env_subprocess_error(
+ mocker,
+ monkeypatch,
+ main_reload_styles,
+ package_test_invalid_requirements,
+ tmp_dir,
+ capsys,
+ color,
+ stdout_body,
+ stdout_error,
+):
+ try:
+ # do not inject hook to have clear output on capsys
+ mocker.patch('colorama.init')
+ except ModuleNotFoundError: # colorama might not be available
+ pass
+
+ monkeypatch.delenv('NO_COLOR', raising=False)
+ monkeypatch.setenv('FORCE_COLOR' if color else 'NO_COLOR', '')
+
+ importlib.reload(build.__main__) # reload module to set _STYLES
+
+ with pytest.raises(SystemExit):
+ build.__main__.main([package_test_invalid_requirements, '-o', tmp_dir])
+ stdout, stderr = capsys.readouterr()
+ stdout, stderr = stdout.splitlines(), stderr.splitlines()
+
+ assert stdout[:4] == stdout_body
+ assert stdout[-1].startswith(stdout_error)
+
+ assert len(stderr) == 1
+ assert stderr[0].startswith('ERROR: Invalid requirement: ')
+
+
+@pytest.mark.parametrize(
+ ('tty', 'env', 'colors'),
+ [
+ (False, {}, build.__main__._NO_COLORS),
+ (True, {}, build.__main__._COLORS),
+ (False, {'NO_COLOR': ''}, build.__main__._NO_COLORS),
+ (True, {'NO_COLOR': ''}, build.__main__._NO_COLORS),
+ (False, {'FORCE_COLOR': ''}, build.__main__._COLORS),
+ (True, {'FORCE_COLOR': ''}, build.__main__._COLORS),
+ ],
+)
+def test_colors(mocker, monkeypatch, main_reload_styles, tty, env, colors):
+ mocker.patch('sys.stdout.isatty', return_value=tty)
+ for key, value in env.items():
+ monkeypatch.setenv(key, value)
+
+ importlib.reload(build.__main__) # reload module to set _STYLES
+
+ assert build.__main__._STYLES == colors
+
+
+def test_colors_conflict(monkeypatch, main_reload_styles):
+ with monkeypatch.context() as m:
+ m.setenv('NO_COLOR', '')
+ m.setenv('FORCE_COLOR', '')
+
+ with pytest.warns(
+ UserWarning,
+ match='Both NO_COLOR and FORCE_COLOR environment variables are set, disabling color',
+ ):
+ importlib.reload(build.__main__)
+
+ assert build.__main__._STYLES == build.__main__._NO_COLORS
+
+
+def raise_called_process_err(*args, **kwargs):
+ raise subprocess.CalledProcessError(1, ['test', 'args'], b'stdoutput', b'stderror')
+
+
+def test_venv_fail(monkeypatch, package_test_flit, tmp_dir, capsys):
+ monkeypatch.setattr(venv.EnvBuilder, 'create', raise_called_process_err)
+ monkeypatch.setenv('NO_COLOR', '')
+
+ importlib.reload(build.__main__) # reload module to set _STYLES
+
+ with pytest.raises(SystemExit):
+ build.__main__.main([package_test_flit, '-o', tmp_dir])
+
+ stdout, stderr = capsys.readouterr()
+
+ assert (
+ stdout
+ == '''\
+* Creating venv isolated environment...
+ERROR Failed to create venv. Maybe try installing virtualenv.
+ Command 'test args' failed with return code 1
+ stdout:
+ stdoutput
+ stderr:
+ stderror
+'''
+ )
+ assert stderr == ''
diff --git a/tests/test_module.py b/tests/test_module.py
new file mode 100644
index 0000000..1d3d1cc
--- /dev/null
+++ b/tests/test_module.py
@@ -0,0 +1,16 @@
+# SPDX-License-Identifier: MIT
+
+import sys
+
+import pytest
+
+import build
+
+
+def test_version():
+ assert build.__version__
+
+
+@pytest.mark.skipif(sys.version_info < (3, 7), reason='Python 3.7+ required for dir support')
+def test_dir():
+ assert set(dir(build)) == set(build.__all__)
diff --git a/tests/test_projectbuilder.py b/tests/test_projectbuilder.py
new file mode 100644
index 0000000..57ff9f9
--- /dev/null
+++ b/tests/test_projectbuilder.py
@@ -0,0 +1,672 @@
+# SPDX-License-Identifier: MIT
+
+
+import copy
+import importlib
+import logging
+import os
+import sys
+import textwrap
+
+import pep517.wrappers
+import pytest
+
+import build
+
+
+if sys.version_info >= (3, 8): # pragma: no cover
+ from importlib import metadata as importlib_metadata
+else: # pragma: no cover
+ import importlib_metadata
+
+import pathlib
+
+
+build_open_owner = 'builtins'
+
+
+DEFAULT_BACKEND = {
+ 'build-backend': 'setuptools.build_meta:__legacy__',
+ 'requires': ['setuptools >= 40.8.0', 'wheel'],
+}
+
+
+class MockDistribution(importlib_metadata.Distribution):
+ def locate_file(self, path): # pragma: no cover
+ return ''
+
+ @classmethod
+ def from_name(cls, name):
+ if name == 'extras_dep':
+ return ExtraMockDistribution()
+ elif name == 'requireless_dep':
+ return RequirelessMockDistribution()
+ elif name == 'recursive_dep':
+ return RecursiveMockDistribution()
+ elif name == 'prerelease_dep':
+ return PrereleaseMockDistribution()
+ elif name == 'circular_dep':
+ return CircularMockDistribution()
+ elif name == 'nested_circular_dep':
+ return NestedCircularMockDistribution()
+ raise importlib_metadata.PackageNotFoundError
+
+
+class ExtraMockDistribution(MockDistribution):
+ def read_text(self, filename):
+ if filename == 'METADATA':
+ return textwrap.dedent(
+ """
+ Metadata-Version: 2.2
+ Name: extras_dep
+ Version: 1.0.0
+ Provides-Extra: extra_without_associated_deps
+ Provides-Extra: extra_with_unmet_deps
+ Requires-Dist: unmet_dep; extra == 'extra_with_unmet_deps'
+ Provides-Extra: extra_with_met_deps
+ Requires-Dist: extras_dep; extra == 'extra_with_met_deps'
+ Provides-Extra: recursive_extra_with_unmet_deps
+ Requires-Dist: recursive_dep; extra == 'recursive_extra_with_unmet_deps'
+ """
+ ).strip()
+
+
+class RequirelessMockDistribution(MockDistribution):
+ def read_text(self, filename):
+ if filename == 'METADATA':
+ return textwrap.dedent(
+ """
+ Metadata-Version: 2.2
+ Name: requireless_dep
+ Version: 1.0.0
+ """
+ ).strip()
+
+
+class RecursiveMockDistribution(MockDistribution):
+ def read_text(self, filename):
+ if filename == 'METADATA':
+ return textwrap.dedent(
+ """
+ Metadata-Version: 2.2
+ Name: recursive_dep
+ Version: 1.0.0
+ Requires-Dist: recursive_unmet_dep
+ """
+ ).strip()
+
+
+class PrereleaseMockDistribution(MockDistribution):
+ def read_text(self, filename):
+ if filename == 'METADATA':
+ return textwrap.dedent(
+ """
+ Metadata-Version: 2.2
+ Name: prerelease_dep
+ Version: 1.0.1a0
+ """
+ ).strip()
+
+
+class CircularMockDistribution(MockDistribution):
+ def read_text(self, filename):
+ if filename == 'METADATA':
+ return textwrap.dedent(
+ """
+ Metadata-Version: 2.2
+ Name: circular_dep
+ Version: 1.0.0
+ Requires-Dist: nested_circular_dep
+ """
+ ).strip()
+
+
+class NestedCircularMockDistribution(MockDistribution):
+ def read_text(self, filename):
+ if filename == 'METADATA':
+ return textwrap.dedent(
+ """
+ Metadata-Version: 2.2
+ Name: nested_circular_dep
+ Version: 1.0.0
+ Requires-Dist: circular_dep
+ """
+ ).strip()
+
+
+@pytest.mark.parametrize(
+ ('requirement_string', 'expected'),
+ [
+ ('extras_dep', None),
+ ('missing_dep', ('missing_dep',)),
+ ('requireless_dep', None),
+ ('extras_dep[undefined_extra]', None),
+ # would the wheel builder filter this out?
+ ('extras_dep[extra_without_associated_deps]', None),
+ (
+ 'extras_dep[extra_with_unmet_deps]',
+ ('extras_dep[extra_with_unmet_deps]', 'unmet_dep; extra == "extra_with_unmet_deps"'),
+ ),
+ (
+ 'extras_dep[recursive_extra_with_unmet_deps]',
+ (
+ 'extras_dep[recursive_extra_with_unmet_deps]',
+ 'recursive_dep; extra == "recursive_extra_with_unmet_deps"',
+ 'recursive_unmet_dep',
+ ),
+ ),
+ ('extras_dep[extra_with_met_deps]', None),
+ ('missing_dep; python_version>"10"', None),
+ ('missing_dep; python_version<="1"', None),
+ ('missing_dep; python_version>="1"', ('missing_dep; python_version >= "1"',)),
+ ('extras_dep == 1.0.0', None),
+ ('extras_dep == 2.0.0', ('extras_dep==2.0.0',)),
+ ('extras_dep[extra_without_associated_deps] == 1.0.0', None),
+ ('extras_dep[extra_without_associated_deps] == 2.0.0', ('extras_dep[extra_without_associated_deps]==2.0.0',)),
+ ('prerelease_dep >= 1.0.0', None),
+ ('circular_dep', None),
+ ],
+)
+def test_check_dependency(monkeypatch, requirement_string, expected):
+ monkeypatch.setattr(importlib_metadata, 'Distribution', MockDistribution)
+ assert next(build.check_dependency(requirement_string), None) == expected
+
+
+def test_bad_project(package_test_no_project):
+ # Passing a nonexistent project directory
+ with pytest.raises(build.BuildException):
+ build.ProjectBuilder(os.path.join(package_test_no_project, 'does-not-exist'))
+ # Passing a file as a project directory
+ with pytest.raises(build.BuildException):
+ build.ProjectBuilder(os.path.join(package_test_no_project, 'empty.txt'))
+ # Passing a project directory with no pyproject.toml or setup.py
+ with pytest.raises(build.BuildException):
+ build.ProjectBuilder(package_test_no_project)
+
+
+def test_init(mocker, package_test_flit, package_legacy, test_no_permission, package_test_bad_syntax):
+ mocker.patch('pep517.wrappers.Pep517HookCaller')
+
+ # correct flit pyproject.toml
+ builder = build.ProjectBuilder(package_test_flit)
+ pep517.wrappers.Pep517HookCaller.assert_called_with(
+ package_test_flit, 'flit_core.buildapi', backend_path=None, python_executable=sys.executable, runner=builder._runner
+ )
+ pep517.wrappers.Pep517HookCaller.reset_mock()
+
+ # custom python
+ builder = build.ProjectBuilder(package_test_flit, python_executable='some-python')
+ pep517.wrappers.Pep517HookCaller.assert_called_with(
+ package_test_flit, 'flit_core.buildapi', backend_path=None, python_executable='some-python', runner=builder._runner
+ )
+ pep517.wrappers.Pep517HookCaller.reset_mock()
+
+ # FileNotFoundError
+ builder = build.ProjectBuilder(package_legacy)
+ pep517.wrappers.Pep517HookCaller.assert_called_with(
+ package_legacy,
+ 'setuptools.build_meta:__legacy__',
+ backend_path=None,
+ python_executable=sys.executable,
+ runner=builder._runner,
+ )
+
+ # PermissionError
+ if not sys.platform.startswith('win'): # can't correctly set the permissions required for this
+ with pytest.raises(build.BuildException):
+ build.ProjectBuilder(test_no_permission)
+
+ # TomlDecodeError
+ with pytest.raises(build.BuildException):
+ build.ProjectBuilder(package_test_bad_syntax)
+
+
+@pytest.mark.parametrize('value', [b'something', 'something_else'])
+def test_python_executable(package_test_flit, value):
+ builder = build.ProjectBuilder(package_test_flit)
+
+ builder.python_executable = value
+ assert builder.python_executable == value
+ assert builder._hook.python_executable == value
+
+
+@pytest.mark.parametrize('distribution', ['wheel', 'sdist'])
+def test_get_requires_for_build_missing_backend(packages_path, distribution):
+ bad_backend_path = os.path.join(packages_path, 'test-bad-backend')
+ builder = build.ProjectBuilder(bad_backend_path)
+
+ with pytest.raises(build.BuildBackendException):
+ builder.get_requires_for_build(distribution)
+
+
+@pytest.mark.parametrize('distribution', ['wheel', 'sdist'])
+def test_get_requires_for_build_missing_optional_hooks(package_test_optional_hooks, distribution):
+ builder = build.ProjectBuilder(package_test_optional_hooks)
+
+ assert builder.get_requires_for_build(distribution) == set()
+
+
+@pytest.mark.parametrize('distribution', ['wheel', 'sdist'])
+def test_build_missing_backend(packages_path, distribution, tmpdir):
+ bad_backend_path = os.path.join(packages_path, 'test-bad-backend')
+ builder = build.ProjectBuilder(bad_backend_path)
+
+ with pytest.raises(build.BuildBackendException):
+ builder.build(distribution, str(tmpdir))
+
+
+def test_check_dependencies(mocker, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller.get_requires_for_build_sdist')
+ mocker.patch('pep517.wrappers.Pep517HookCaller.get_requires_for_build_wheel')
+
+ builder = build.ProjectBuilder(package_test_flit)
+
+ side_effects = [
+ [],
+ ['something'],
+ pep517.wrappers.BackendUnavailable,
+ ]
+
+ builder._hook.get_requires_for_build_sdist.side_effect = copy.copy(side_effects)
+ builder._hook.get_requires_for_build_wheel.side_effect = copy.copy(side_effects)
+
+ # requires = []
+ assert builder.check_dependencies('sdist') == {('flit_core<3,>=2',)}
+ assert builder.check_dependencies('wheel') == {('flit_core<3,>=2',)}
+
+ # requires = ['something']
+ assert builder.check_dependencies('sdist') == {('flit_core<3,>=2',), ('something',)}
+ assert builder.check_dependencies('wheel') == {('flit_core<3,>=2',), ('something',)}
+
+ # BackendUnavailable
+ with pytest.raises(build.BuildBackendException):
+ builder.check_dependencies('sdist')
+ with pytest.raises(build.BuildBackendException):
+ not builder.check_dependencies('wheel')
+
+
+def test_working_directory(tmp_dir):
+ assert os.path.realpath(os.curdir) != os.path.realpath(tmp_dir)
+ with build._working_directory(tmp_dir):
+ assert os.path.realpath(os.curdir) == os.path.realpath(tmp_dir)
+
+
+def test_working_directory_exc_is_not_transformed(mocker, package_test_flit, tmp_dir):
+ mocker.patch('build._working_directory', side_effect=OSError)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ with pytest.raises(OSError):
+ builder._call_backend('build_sdist', tmp_dir)
+
+
+def test_build(mocker, package_test_flit, tmp_dir):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+ mocker.patch('build._working_directory', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+
+ builder._hook.build_sdist.side_effect = ['dist.tar.gz', Exception]
+ builder._hook.build_wheel.side_effect = ['dist.whl', Exception]
+
+ assert builder.build('sdist', tmp_dir) == os.path.join(tmp_dir, 'dist.tar.gz')
+ builder._hook.build_sdist.assert_called_with(tmp_dir, None)
+ build._working_directory.assert_called_with(package_test_flit)
+
+ assert builder.build('wheel', tmp_dir) == os.path.join(tmp_dir, 'dist.whl')
+ builder._hook.build_wheel.assert_called_with(tmp_dir, None)
+ build._working_directory.assert_called_with(package_test_flit)
+
+ with pytest.raises(build.BuildBackendException):
+ build._working_directory.assert_called_with(package_test_flit)
+ builder.build('sdist', tmp_dir)
+
+ with pytest.raises(build.BuildBackendException):
+ build._working_directory.assert_called_with(package_test_flit)
+ builder.build('wheel', tmp_dir)
+
+
+def test_default_backend(mocker, package_legacy):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_legacy)
+
+ assert builder._build_system == DEFAULT_BACKEND
+
+
+def test_missing_backend(mocker, package_test_no_backend):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_no_backend)
+
+ assert builder._build_system == {'requires': [], 'build-backend': DEFAULT_BACKEND['build-backend']}
+
+
+def test_missing_requires(mocker, package_test_no_requires):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ with pytest.raises(build.BuildException):
+ build.ProjectBuilder(package_test_no_requires)
+
+
+def test_build_system_typo(mocker, package_test_typo):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ with pytest.warns(build.TypoWarning):
+ build.ProjectBuilder(package_test_typo)
+
+
+def test_missing_outdir(mocker, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ builder._hook.build_sdist.return_value = 'dist.tar.gz'
+ out = os.path.join(tmp_dir, 'out')
+
+ builder.build('sdist', out)
+
+ assert os.path.isdir(out)
+
+
+def test_relative_outdir(mocker, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ builder._hook.build_sdist.return_value = 'dist.tar.gz'
+
+ builder.build('sdist', '.')
+
+ builder._hook.build_sdist.assert_called_with(os.path.abspath('.'), None)
+
+
+def test_build_not_dir_outdir(mocker, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ builder._hook.build_sdist.return_value = 'dist.tar.gz'
+ out = os.path.join(tmp_dir, 'out')
+
+ open(out, 'a').close() # create empty file
+
+ with pytest.raises(build.BuildException):
+ builder.build('sdist', out)
+
+
+@pytest.fixture(scope='session')
+def demo_pkg_inline(tmp_path_factory):
+ # builds a wheel without any dependencies and with a console script demo-pkg-inline
+ tmp_path = tmp_path_factory.mktemp('demo-pkg-inline')
+ builder = build.ProjectBuilder(srcdir=os.path.join(os.path.dirname(__file__), 'packages', 'inline'))
+ out = tmp_path / 'dist'
+ builder.build('wheel', str(out))
+ return next(out.iterdir())
+
+
+@pytest.mark.isolated
+def test_build_with_dep_on_console_script(tmp_path, demo_pkg_inline, capfd, mocker):
+ """
+ All command-line scripts provided by the build-required packages must be present in the build environment's PATH.
+ """
+ # we first install demo pkg inline as build dependency (as this provides a console script we can check)
+ # to validate backend invocations contain the correct path we use an inline backend that will fail, but first
+ # provides the PATH information (and validates shutil.which is able to discover the executable - as PEP states)
+ toml = textwrap.dedent(
+ '''
+ [build-system]
+ requires = ["demo_pkg_inline"]
+ build-backend = "build"
+ backend-path = ["."]
+
+ [project]
+ description = "Factory ⸻ A code generator 🏭"
+ authors = [{name = "Łukasz Langa"}]
+ '''
+ )
+ code = textwrap.dedent(
+ '''
+ import os
+ import shutil
+ import sys
+ print("BB " + os.environ["PATH"])
+ exe_at = shutil.which("demo-pkg-inline")
+ print("BB " + exe_at)
+ '''
+ )
+ (tmp_path / 'pyproject.toml').write_text(toml, encoding='UTF-8')
+ (tmp_path / 'build.py').write_text(code)
+
+ deps = {str(demo_pkg_inline)} # we patch the requires demo_pkg_inline to refer to the wheel -> we don't need index
+ mocker.patch('build.ProjectBuilder.build_system_requires', new_callable=mocker.PropertyMock, return_value=deps)
+ from build.__main__ import main
+
+ with pytest.raises(SystemExit):
+ main(['--wheel', '--outdir', str(tmp_path / 'dist'), str(tmp_path)])
+
+ out, err = capfd.readouterr()
+ lines = [line[3:] for line in out.splitlines() if line.startswith('BB ')] # filter for our markers
+ path_vars = lines[0].split(os.pathsep)
+ which_detected = lines[1]
+ assert which_detected.startswith(path_vars[0]), out
+
+
+def test_prepare(mocker, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+ mocker.patch('build._working_directory', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ builder._hook.prepare_metadata_for_build_wheel.return_value = 'dist-1.0.dist-info'
+
+ assert builder.prepare('wheel', tmp_dir) == os.path.join(tmp_dir, 'dist-1.0.dist-info')
+ builder._hook.prepare_metadata_for_build_wheel.assert_called_with(tmp_dir, None, _allow_fallback=False)
+ build._working_directory.assert_called_with(package_test_flit)
+
+
+def test_prepare_no_hook(mocker, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ failure = pep517.wrappers.HookMissing('prepare_metadata_for_build_wheel')
+ builder._hook.prepare_metadata_for_build_wheel.side_effect = failure
+
+ assert builder.prepare('wheel', tmp_dir) is None
+
+
+def test_prepare_error(mocker, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ builder._hook.prepare_metadata_for_build_wheel.side_effect = Exception
+
+ with pytest.raises(build.BuildBackendException, match='Backend operation failed: Exception'):
+ builder.prepare('wheel', tmp_dir)
+
+
+def test_prepare_not_dir_outdir(mocker, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+
+ out = os.path.join(tmp_dir, 'out')
+ with open(out, 'w') as f:
+ f.write('Not a directory')
+ with pytest.raises(build.BuildException, match='Build path .* exists and is not a directory'):
+ builder.prepare('wheel', out)
+
+
+def test_no_outdir_single(mocker, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller.prepare_metadata_for_build_wheel', return_value='')
+
+ builder = build.ProjectBuilder(package_test_flit)
+
+ out = os.path.join(tmp_dir, 'out')
+ builder.prepare('wheel', out)
+
+ assert os.path.isdir(out)
+
+
+def test_no_outdir_multiple(mocker, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller.prepare_metadata_for_build_wheel', return_value='')
+
+ builder = build.ProjectBuilder(package_test_flit)
+
+ out = os.path.join(tmp_dir, 'does', 'not', 'exist')
+ builder.prepare('wheel', out)
+
+ assert os.path.isdir(out)
+
+
+def test_runner_user_specified(tmp_dir, package_test_flit):
+ def dummy_runner(cmd, cwd=None, env=None):
+ raise RuntimeError('Runner was called')
+
+ builder = build.ProjectBuilder(package_test_flit, runner=dummy_runner)
+ with pytest.raises(build.BuildBackendException, match='Runner was called'):
+ builder.build('wheel', tmp_dir)
+
+
+def test_metadata_path_no_prepare(tmp_dir, package_test_no_prepare):
+ builder = build.ProjectBuilder(package_test_no_prepare)
+
+ metadata = importlib_metadata.PathDistribution(
+ pathlib.Path(builder.metadata_path(tmp_dir)),
+ ).metadata
+
+ assert metadata['name'] == 'test-no-prepare'
+ assert metadata['Version'] == '1.0.0'
+
+
+def test_metadata_path_with_prepare(tmp_dir, package_test_setuptools):
+ builder = build.ProjectBuilder(package_test_setuptools)
+
+ metadata = importlib_metadata.PathDistribution(
+ pathlib.Path(builder.metadata_path(tmp_dir)),
+ ).metadata
+
+ assert metadata['name'] == 'test-setuptools'
+ assert metadata['Version'] == '1.0.0'
+
+
+def test_metadata_path_legacy(tmp_dir, package_legacy):
+ builder = build.ProjectBuilder(package_legacy)
+
+ metadata = importlib_metadata.PathDistribution(
+ pathlib.Path(builder.metadata_path(tmp_dir)),
+ ).metadata
+
+ assert metadata['name'] == 'legacy'
+ assert metadata['Version'] == '1.0.0'
+
+
+def test_metadata_invalid_wheel(tmp_dir, package_test_bad_wheel):
+ builder = build.ProjectBuilder(package_test_bad_wheel)
+
+ with pytest.raises(ValueError, match='Invalid wheel'):
+ builder.metadata_path(tmp_dir)
+
+
+@pytest.fixture
+def mock_tomli_not_available(mocker):
+ loads = mocker.patch('tomli.loads')
+ mocker.patch.dict(sys.modules, {'tomli': None})
+ importlib.reload(build)
+ try:
+ yield
+ finally:
+ loads.assert_not_called()
+ mocker.stopall()
+ importlib.reload(build)
+
+
+@pytest.mark.skipif(sys.version_info >= (3, 11), reason='No need to test old toml support on 3.11+')
+def test_toml_instead_of_tomli(mocker, mock_tomli_not_available, tmp_dir, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ builder._hook.build_sdist.return_value = 'dist.tar.gz'
+
+ builder.build('sdist', '.')
+
+ builder._hook.build_sdist.assert_called_with(os.path.abspath('.'), None)
+
+
+def test_log(mocker, caplog, package_test_flit):
+ mocker.patch('pep517.wrappers.Pep517HookCaller', autospec=True)
+ mocker.patch('build.ProjectBuilder._call_backend', return_value='some_path')
+ caplog.set_level(logging.DEBUG)
+
+ builder = build.ProjectBuilder(package_test_flit)
+ builder.get_requires_for_build('sdist')
+ builder.get_requires_for_build('wheel')
+ builder.prepare('wheel', '.')
+ builder.build('sdist', '.')
+ builder.build('wheel', '.')
+ builder.log('something')
+
+ assert [(record.levelname, record.message) for record in caplog.records] == [
+ ('INFO', 'Getting build dependencies for sdist...'),
+ ('INFO', 'Getting build dependencies for wheel...'),
+ ('INFO', 'Getting metadata for wheel...'),
+ ('INFO', 'Building sdist...'),
+ ('INFO', 'Building wheel...'),
+ ('INFO', 'something'),
+ ]
+ if sys.version_info >= (3, 8): # stacklevel
+ assert caplog.records[-1].lineno == 602
+
+
+@pytest.mark.parametrize(
+ ('pyproject_toml', 'parse_output'),
+ [
+ (
+ {'build-system': {'requires': ['foo']}},
+ {'requires': ['foo'], 'build-backend': 'setuptools.build_meta:__legacy__'},
+ ),
+ (
+ {'build-system': {'requires': ['foo'], 'build-backend': 'bar'}},
+ {'requires': ['foo'], 'build-backend': 'bar'},
+ ),
+ (
+ {'build-system': {'requires': ['foo'], 'build-backend': 'bar', 'backend-path': ['baz']}},
+ {'requires': ['foo'], 'build-backend': 'bar', 'backend-path': ['baz']},
+ ),
+ ],
+)
+def test_parse_valid_build_system_table_type(pyproject_toml, parse_output):
+ assert build._parse_build_system_table(pyproject_toml) == parse_output
+
+
+@pytest.mark.parametrize(
+ ('pyproject_toml', 'error_message'),
+ [
+ (
+ {'build-system': {}},
+ '`requires` is a required property',
+ ),
+ (
+ {'build-system': {'requires': 'not an array'}},
+ '`requires` must be an array of strings',
+ ),
+ (
+ {'build-system': {'requires': [1]}},
+ '`requires` must be an array of strings',
+ ),
+ (
+ {'build-system': {'requires': ['foo'], 'build-backend': ['not a string']}},
+ '`build-backend` must be a string',
+ ),
+ (
+ {'build-system': {'requires': ['foo'], 'backend-path': 'not an array'}},
+ '`backend-path` must be an array of strings',
+ ),
+ (
+ {'build-system': {'requires': ['foo'], 'backend-path': [1]}},
+ '`backend-path` must be an array of strings',
+ ),
+ (
+ {'build-system': {'requires': ['foo'], 'unknown-prop': False}},
+ 'Unknown properties: unknown-prop',
+ ),
+ ],
+)
+def test_parse_invalid_build_system_table_type(pyproject_toml, error_message):
+ with pytest.raises(build.BuildSystemTableValidationError, match=error_message):
+ build._parse_build_system_table(pyproject_toml)
diff --git a/tests/test_self_packaging.py b/tests/test_self_packaging.py
new file mode 100644
index 0000000..abe09f8
--- /dev/null
+++ b/tests/test_self_packaging.py
@@ -0,0 +1,103 @@
+# These tests check the sdist, path, and wheel of build to ensure that all are valid.
+
+import subprocess
+import sys
+import tarfile
+import zipfile
+
+from pathlib import Path
+
+import pytest
+
+
+DIR = Path(__file__).parent.resolve()
+MAIN_DIR = DIR.parent
+
+sdist_files = {
+ 'LICENSE',
+ 'PKG-INFO',
+ 'README.md',
+ 'pyproject.toml',
+ 'setup.cfg',
+ 'setup.py',
+ 'src',
+ 'src/build',
+ 'src/build.egg-info',
+ 'src/build.egg-info/PKG-INFO',
+ 'src/build.egg-info/SOURCES.txt',
+ 'src/build.egg-info/dependency_links.txt',
+ 'src/build.egg-info/entry_points.txt',
+ 'src/build.egg-info/requires.txt',
+ 'src/build.egg-info/top_level.txt',
+ 'src/build/__init__.py',
+ 'src/build/__main__.py',
+ 'src/build/env.py',
+ 'src/build/py.typed',
+ 'src/build/util.py',
+}
+
+wheel_files = {
+ 'build/__init__.py',
+ 'build/__main__.py',
+ 'build/env.py',
+ 'build/py.typed',
+ 'build/util.py',
+ 'dist-info/LICENSE',
+ 'dist-info/METADATA',
+ 'dist-info/RECORD',
+ 'dist-info/WHEEL',
+ 'dist-info/entry_points.txt',
+ 'dist-info/top_level.txt',
+}
+
+
+def test_build_sdist(monkeypatch, tmpdir):
+
+ monkeypatch.chdir(MAIN_DIR)
+
+ subprocess.run(
+ [
+ sys.executable,
+ '-m',
+ 'build',
+ '--sdist',
+ '--outdir',
+ str(tmpdir),
+ ],
+ check=True,
+ ).stdout
+
+ (sdist,) = tmpdir.visit('*.tar.gz')
+
+ with tarfile.open(str(sdist), 'r:gz') as tar:
+ simpler = {n.split('/', 1)[-1] for n in tar.getnames()[1:]}
+
+ assert simpler == sdist_files
+
+
+@pytest.mark.parametrize('args', ((), ('--wheel',)), ids=('from_sdist', 'direct'))
+def test_build_wheel(monkeypatch, tmpdir, args):
+
+ monkeypatch.chdir(MAIN_DIR)
+
+ subprocess.run(
+ [
+ sys.executable,
+ '-m',
+ 'build',
+ *args,
+ '--outdir',
+ str(tmpdir),
+ ],
+ check=True,
+ )
+
+ (wheel,) = tmpdir.visit('*.whl')
+
+ with zipfile.ZipFile(str(wheel)) as z:
+ names = z.namelist()
+
+ trimmed = {n for n in names if 'dist-info' not in n}
+ trimmed |= {f"dist-info/{n.split('/', 1)[-1]}" for n in names if 'dist-info' in n}
+
+ assert trimmed == wheel_files
diff --git a/tests/test_util.py b/tests/test_util.py
new file mode 100644
index 0000000..9f090b5
--- /dev/null
+++ b/tests/test_util.py
@@ -0,0 +1,44 @@
+# SPDX-License-Identifier: MIT
+
+import pytest
+
+import build.util
+
+
+@pytest.mark.pypy3323bug
+@pytest.mark.parametrize('isolated', [False, True])
+def test_wheel_metadata(package_test_setuptools, isolated):
+ metadata = build.util.project_wheel_metadata(package_test_setuptools, isolated)
+
+ assert metadata['name'] == 'test-setuptools'
+ assert metadata['version'] == '1.0.0'
+
+
+@pytest.mark.pypy3323bug
+def test_wheel_metadata_isolation(package_test_flit):
+ try:
+ import flit_core # noqa: F401
+ except ModuleNotFoundError:
+ pass
+ else:
+ pytest.xfail('flit_core is available -- we want it missing!') # pragma: no cover
+
+ metadata = build.util.project_wheel_metadata(package_test_flit)
+
+ assert metadata['name'] == 'test_flit'
+ assert metadata['version'] == '1.0.0'
+
+ with pytest.raises(
+ build.BuildBackendException,
+ match="Backend 'flit_core.buildapi' is not available.",
+ ):
+ build.util.project_wheel_metadata(package_test_flit, isolated=False)
+
+
+@pytest.mark.pypy3323bug
+def test_with_get_requires(package_test_metadata):
+ metadata = build.util.project_wheel_metadata(package_test_metadata)
+
+ assert metadata['name'] == 'test-metadata'
+ assert str(metadata['version']) == '1.0.0'
+ assert metadata['summary'] == 'hello!'